Engines¶
src.engines
¶
src.engines.base_engine
¶
Abstract base engine for multi-backend LLM inference.
This module provides the foundation for Erudi's multi-engine architecture, supporting MLX (Apple Silicon), CUDA (NVIDIA GPUs), and CPU fallback. The engine abstraction enables consistent model loading, quantization, and streaming inference across different hardware backends.
Fonctionnalités: - Automatic engine selection based on platform and hardware - Singleton pattern with shared model state across requests - Automatic memory cleanup after idle timeout - Streaming token generation API - Engine-format gate (FORMAT_TAG): only pre-built artefacts are ever loaded
Architecture
BaseEngine (ABC) ├── MLX_Engine (Apple Silicon) ├── CUDA_Engine (NVIDIA GPUs) └── CPU_Engine (Fallback)
Examples:
Get appropriate engine for current platform: from src.engines.base_engine import BaseEngine engine_class = BaseEngine.get_engine() model, tokenizer = engine_class.get_model_and_tokenizer( llm_id="1", llm_local_path="backend/data/models/llama-7b" )
Stream tokens (driven by the agent layer, not the engine):
the model handle's base_url is consumed by ChatOpenAI in
src.agents.model_factory; the engine only spawns / probes /
reaps the OpenAI-compatible server.
BaseEngine()
¶
Bases: ABC
Abstract base class for all LLM inference engines.
Implements singleton pattern with shared model state, automatic cleanup, and platform-aware engine selection. Subclasses must implement backend- specific model loading and inference methods.
Class Attributes
_model: Currently loaded model instance (shared across all engines). _tokenizer: Currently loaded tokenizer instance. _model_id: ID of the currently loaded model. _last_used: Timestamp of last model access for cleanup tracking. _cleanup_task: Async task monitoring idle time. _max_idle_time: Seconds before automatic memory cleanup (default: 300). MODEL_MAPPING: Dictionary mapping model architectures to classes.
Context windows -- two numbers that must never be confused:
max_context_tokens()is the declared ceiling: the window the configuration imposes on every spawn (ERUDI_CTXwhen the user pinned one), orNonewhen nothing is imposed. It exists before any model is loaded and never depends on which model is.effective_context_tokens()is the allocated window: what the CURRENTLY LOADED child actually runs with, read from the live engine handle. It only exists while a child is up, differs per model AND per machine/memory state (llama-server's fit can shrink it below the model's trained window), and is therefore never stored in the database.
Consumers that budget or warn against "the window" (compaction thresholds,
the first-token watchdog, the UI's allocated-window display) must read the
allocated window; only the sampling resolver's max_tokens_cap reads the
declared ceiling, because it describes configuration, not a running child.
Note
Do not instantiate directly. Use get_engine() to obtain the appropriate engine class, then call class methods for operations.
Prevent direct instantiation.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Always raised as engines use class methods only. |
Source code in backend/src/engines/base_engine.py
begin_memory_window()
classmethod
¶
Open a measurement window on the loaded child for one turn; the
returned token closes it (end_memory_window). No-op here.
brake_point(point, *, llm_id=None)
classmethod
¶
Take the brake's sample at a safe point (turn_start for
llm_id, turn_end, idle) and act on it. No-op here.
Source code in backend/src/engines/base_engine.py
brake_reload_pending(llm_id)
classmethod
¶
Whether the next turn for llm_id reloads a model the brake
unloaded (or, on the CPU engine, re-prefills a child it respawned).
Never here.
chat_template_caps(llm_id=None)
classmethod
¶
What the CURRENTLY LOADED child reported about its chat template.
llama.cpp engines stamp the server's chat_template_caps map on the
handle during the one /props read at spawn -- nine booleans
evaluated by llama.cpp's own symbolic execution of the template, of
which supports_reasoning_effort drives the reasoning-effort lever.
Engines whose server reports nothing (MLX) return an empty map, and so
does a child that is down or serving another model.
llm_id guards exactly that: capabilities belong to the artifact the
child was spawned for, so a caller planning a turn for a DIFFERENT
model gets nothing rather than the loaded model's answers.
Source code in backend/src/engines/base_engine.py
claim_prefix(owner)
classmethod
¶
Make the loaded child's prefix cache belong to owner
(conv:<thread id> or arena), resetting it when it held
something else. No-op here.
cleanup()
classmethod
¶
Free model and tokenizer from memory, reset state.
Releases GPU/CPU memory occupied by the model and tokenizer, resets all cached state. Called automatically after idle timeout or manually when switching models.
Note
Thread-safe. Can be called explicitly or by cleanup monitor. Calls garbage collector to ensure memory is freed.
Source code in backend/src/engines/base_engine.py
community_search_kwargs(term)
classmethod
¶
HfApi.list_models kwargs to find models in THIS engine's runnable
format across ALL of HF (any author), so community fine-tunes surface and
are runnable by construction. Searches by the format TAG, not by org —
e.g. filter="mlx" catches non-mlx-community quants the org never holds.
An empty term is the global pass: filter by format only, ranked by
downloads.
expand pulls pipeline_tag + tags (alongside the downloads/likes the
quality floor needs) in the one call, so the non-chat task filter (#242)
can gate ASR/embedding/OCR/media-gen repos out of the derived catalog,
plus gated so a repo the token-less app cannot download is dropped
(model_resolver.is_gated).
Source code in backend/src/engines/base_engine.py
compute_supports_tools(local_path)
classmethod
¶
Static tool-calling capability of the model at local_path.
Computed once at post-download time from the model's chat template by
differential rendering (see engines.tool_capability): no inference,
no runtime probe. Any failure (unreadable artifact, missing template)
returns False so the model routes through the systematic KB path —
the capability is never assumed.
Source code in backend/src/engines/base_engine.py
compute_wire_tools(local_path)
classmethod
¶
Verified tool-call WIRE capability of the model at local_path (#298).
compute_supports_tools says the chat template DECLARES tools; this
says the engine's local server actually parses the model's tool-call
output into a structured call the agent executes — a per-model wire
property (#273 matrix): a template matching no parser streams the call
as raw text (swallowed answer, #295) or leaks JSON to the user.
Engine-specific: MLX runs mlx-vlm's own parser inference on the chat
template; llama.cpp engines trust the --jinja generic fallback for
any usable template. The base default is None (unverified) so an
engine without an implementation never routes a KB turn agentic.
Source code in backend/src/engines/base_engine.py
consume_brake_notice()
classmethod
¶
effective_context_tokens()
classmethod
¶
The ALLOCATED WINDOW: the context window of the CURRENTLY LOADED
child, in tokens, or None when no child is up or the window could
not be read.
Read from the live engine handle (_model): llama.cpp engines stamp
it from the server's /props after boot (the CPU engine's -c,
sized from the RAM budget at spawn and reduced by its memory brake; on
CUDA, llama-server's own fit, which may shrink it below the model's
trained window when memory demands); MLX stamps the trained window it passed at spawn as
the preflight bound, then lowers it to the memory bound once the
child's base footprint is measured, when memory binds
(memory_bound_tokens). Never persisted anywhere -- the same model
gets a different window on a different machine or memory state, so the
only truthful source is the running child.
Source code in backend/src/engines/base_engine.py
end_memory_window(token, *, abandoned=False, compaction_threshold=None)
classmethod
¶
Close the window token opened and record what the turn used,
with the compaction threshold of the conversation that ran it
(None: an arena turn). No-op here.
Source code in backend/src/engines/base_engine.py
generation_guard()
async
classmethod
¶
Serialize a full generation and suppress idle cleanup for its duration.
The agent layer wraps model resolution + the entire token stream in this
guard so that:
- concurrent requests for different models can't thrash the
single-model engine subprocess (they serialize on one asyncio lock);
- the idle-cleanup tick (_cleanup_tick) shares this SAME lock, so
it cannot run — let alone reap the model — while a generation is in
flight. It waits until the guard exits, by which point the idle
clock has been refreshed, so no mid-generation teardown is possible.
This is the engine-level home of the invariant that generate_stream
used to carry; it lives here (not in the agent layer) to keep subprocess
lifecycle and concurrency inside the engine encapsulation.
The lock is taken through _acquire_generation_lock, which first
drains a prefix-cache reset an earlier holder left running.
Source code in backend/src/engines/base_engine.py
get_engine()
classmethod
¶
Select and return appropriate engine class for current platform.
Automatically detects OS and hardware to choose the optimal backend: - macOS ARM (M1/M2/M3): MLX_Engine - macOS Intel: CPU_Engine - Linux/Windows with CUDA: CUDA_Engine - Linux/Windows without CUDA: CPU_Engine
Returns:
| Type | Description |
|---|---|
Type[BaseEngine]
|
The engine class (not an instance) appropriate for this system. |
Raises:
| Type | Description |
|---|---|
EngineException
|
If no suitable engine can be determined. |
Examples:
from src.engines.base_engine import BaseEngine engine_class = BaseEngine.get_engine() print(f"Using: {engine_class}") # e.g., "LLM Engine: MLX_Engine"
Source code in backend/src/engines/base_engine.py
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 | |
get_flat_hardware_data()
abstractmethod
classmethod
¶
Get hardware data in flat format compatible with HardwareProfile entity.
Returns hardware specifications as a flat dictionary ready for direct insertion into the HardwareProfile database entity. Combines and flattens data from get_hardware_info() and get_performance_evaluation().
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict with keys matching HardwareProfile columns: |
Dict[str, Any]
|
{ Common fields (all backends)¶"backend_type": str, # "mlx", "cuda", "cpu" "cpu_model": str, "total_memory_gb": float, "available_memory_gb": float, "disk_total_gb": float, "disk_available_gb": float, "global_inference_score": float, "global_inference_label": str, "cpu_score": float, "memory_score": float, "gpu_score": float, "system_platform": str, "architecture": Optional[str], "estimated_tflops": Optional[float], "memory_bandwidth_gbs": Optional[float], "cpu_performance_units": Optional[float], "performance_breakdown": dict, MLX-specific fields¶"mlx_chip_model": Optional[str], "mlx_gpu_cores": Optional[int], "mps_available": Optional[bool], "neural_engine_tops": Optional[float], "unified_memory": Optional[bool], "gpu_name": Optional[str], CUDA-specific fields¶"cuda_cores": Optional[int], "cuda_version": Optional[str], "compute_capability": Optional[str], "vram_total_gb": Optional[float], "vram_available_gb": Optional[float], |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
EngineException
|
If hardware data collection fails. |
Note
This method provides a single point of access for hardware data in the format expected by the database layer, eliminating the need for manual flattening in service layers.
Examples:
>>> engine = BaseEngine.get_engine()
>>> data = engine.get_flat_hardware_data()
>>> profile = HardwareProfile(**data) # Direct instantiation
>>> db.add(profile)
Source code in backend/src/engines/base_engine.py
get_hardware_info()
abstractmethod
classmethod
¶
Get comprehensive hardware information for this backend.
Returns detailed hardware specifications including CPU, GPU/accelerator, memory, and platform-specific details. Implementation varies by backend: - MLX: Apple Silicon chip model, unified memory, MPS availability - CUDA: NVIDIA GPU model, CUDA cores, separate VRAM - CPU: Processor model, core count, system RAM
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing hardware specifications with the following structure: |
Dict[str, Any]
|
{ "system": { "platform": str, # "Darwin", "Linux", "Windows" "platform_version": Optional[str], "machine": str, # "arm64", "x86_64", etc. "processor": str }, "cpu": { "model": str, "architecture": str, "total_cores": int, "logical_cores": int, "is_apple_silicon": bool, # MLX only "performance_cores": Optional[int], # MLX only "efficiency_cores": Optional[int], # MLX only }, "memory": { "total_memory_gb": float, "available_memory_gb": float, "memory_pressure": float, # 0.0-1.0 "memory_type": str, # "unified" (MLX) or "system" (CPU/CUDA) }, "gpu": { "gpu_name": str, "gpu_cores": Optional[int], # MLX: GPU cores, CUDA: CUDA cores "memory_bandwidth_gbs": Optional[float], "vram_total_gb": Optional[float], # CUDA only "vram_available_gb": Optional[float], # CUDA only "compute_capability": Optional[str], # CUDA only "cuda_version": Optional[str], # CUDA only "mps_supported": Optional[bool], # MLX only "unified_memory": bool, # True for MLX, False for CUDA/CPU }, "accelerator": { # MLX only "neural_engine_tops": Optional[float], "architecture": Optional[str], # "3nm", "5nm", etc. }, "storage": { "total_gb": float, "available_gb": float, "usage_percentage": float }, "backend_type": str, # "mlx", "cuda", or "cpu" "timestamp": float |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
EngineException
|
If hardware detection fails critically. |
Note
Implementation should handle errors gracefully and return fallback values rather than raising exceptions for non-critical failures.
Examples:
>>> engine = BaseEngine.get_engine()
>>> hw_info = engine.get_hardware_info()
>>> print(f"GPU: {hw_info['gpu']['gpu_name']}")
>>> print(f"Total Memory: {hw_info['memory']['total_memory_gb']} GB")
Source code in backend/src/engines/base_engine.py
get_model_and_tokenizer(llm_id, llm_local_path, *args)
abstractmethod
classmethod
¶
Load or retrieve cached model and tokenizer for inference.
Implements singleton pattern: returns cached model if already loaded for the given llm_id, otherwise loads from disk. Thread-safe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
str
|
Unique identifier for the model (used for caching). |
required |
llm_local_path
|
Union[str, Path]
|
Path to the model directory on disk. |
required |
*args
|
Engine-specific loading arguments (e.g., device, dtype). |
()
|
Returns:
| Type | Description |
|---|---|
Tuple[Any, Any]
|
Tuple of (model, tokenizer) ready for inference. |
Raises:
| Type | Description |
|---|---|
EngineException
|
If model loading fails. |
FileNotFoundError
|
If model path doesn't exist. |
Examples:
from src.engines.base_engine import BaseEngine engine = BaseEngine.get_engine() model, tokenizer = engine.get_model_and_tokenizer( llm_id="1", llm_local_path="backend/data/models/llama-7b" )
Source code in backend/src/engines/base_engine.py
get_performance_evaluation()
abstractmethod
classmethod
¶
Calculate comprehensive performance metrics and scores for this backend.
Evaluates hardware capabilities and returns performance scores for inference workloads. Scoring methodology varies by backend but results are normalized to 0-100 scale for cross-platform comparison.
Scoring Components
- Inference Score: Optimized for generation speed and latency. Weights: GPU/accelerator compute (35-60%), memory bandwidth (20-30%), memory capacity (10-30%), CPU (5-10%).
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing performance metrics and scores: |
Dict[str, Any]
|
{ Hardware identification¶"backend_type": str, # "mlx", "cuda", "cpu" "accelerator_name": str, # GPU/chip model "cpu_model": str, Memory metrics¶"total_memory_gb": float, "available_memory_gb": float, "memory_bandwidth_gbs": Optional[float], Storage metrics¶"disk_total_gb": float, "disk_available_gb": float, Compute metrics¶"estimated_tflops": Optional[float], # GPU compute power "compute_units": Optional[int], # GPU cores or CUDA cores "cpu_performance_units": float, Backend-specific metrics¶"neural_engine_tops": Optional[float], # MLX only "cuda_version": Optional[str], # CUDA only "compute_capability": Optional[str], # CUDA only "architecture": Optional[str], # MLX: "3nm", CUDA: "Ampere" Performance scores (0-100)¶"global_inference_score": float, "global_inference_label": str, # "Very Good", "Good", "Medium", "Poor" "gpu_score": float, "cpu_score": float, "memory_score": float, Technical details¶"unified_memory": bool, "accelerator_available": bool, "system_platform": str, Performance breakdown for debugging¶"performance_breakdown": { "compute_score": float, "memory_bandwidth_score": float, "memory_capacity_score": float, "cpu_performance_score": float, # Backend-specific breakdown } |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
EngineException
|
If evaluation fails critically. |
Note
Scores are platform-specific estimates based on hardware specs. Should call warm_up_accelerator() before evaluation for accuracy. Returns fallback scores if evaluation fails rather than raising.
Examples:
>>> engine = BaseEngine.get_engine()
>>> engine.warm_up_accelerator(1.5)
>>> eval_result = engine.get_performance_evaluation()
>>> print(f"Inference: {eval_result['global_inference_score']}/100")
Source code in backend/src/engines/base_engine.py
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 | |
is_runnable(link)
classmethod
¶
Whether a catalog model (already engine-format by construction, since it came from a filter=FORMAT_TAG search) can actually run. The only exclusion is KNOWN_BROKEN — quants that load-crash on this engine.
Source code in backend/src/engines/base_engine.py
max_context_tokens()
classmethod
¶
The DECLARED CEILING: the configured context window imposed on every
spawn, or None when none is configured (the default everywhere).
llama.cpp engines override this to return ERUDI_CTX when the user
pinned one (then passed as -c); MLX never imposes one. This is a
configuration fact, not a runtime one -- for the window the loaded
child actually runs with, read effective_context_tokens(). The
per-model sampling resolver (#388) caps max_tokens on this value
when it exists.
Source code in backend/src/engines/base_engine.py
memory_bound_footprint_bytes()
classmethod
¶
What a memory-bound child holds whatever the conversation does
(the CPU engine's whole allocation at load: KV, compute, weights), the
number the bound memory warning quotes when no per-token
accounting exists; None here and on MLX, whose footprint grows
with the conversation (MemoryBudget.footprint_bytes).
Source code in backend/src/engines/base_engine.py
memory_bound_tokens()
classmethod
¶
The window the machine's MEMORY imposed on the loaded child, or
None when memory is not what bounds it.
Set by an engine whose child's window memory limits: MLX, which lowers
its child's window to the memory ceiling after load, the CPU engine,
which sizes -c from the RAM budget at spawn (never for a
sliding-window model, whose window the budget over-estimates), and
the CUDA engine, which sizes it from the VRAM budget at spawn; it then
equals the allocated window. None means the model's own
window binds (or the bound could not be applied): no memory notice, no
memory wording on an overflow and no memory reason on a compaction
apply.
Source code in backend/src/engines/base_engine.py
model_supports_vision(local_path)
classmethod
¶
Whether the model at local_path accepts image input (#133).
Deterministic, no model load: llama.cpp engines look for an mmproj
projector, MLX reads config.json. Returns None when the engine
cannot decide; the runtime treats None as not vision-capable (#212):
images are stripped unless the capability is an explicit True,
and the user is notified when the current turn carried an image.
Source code in backend/src/engines/base_engine.py
note_call_start(handle, estimated_prompt_tokens)
classmethod
¶
A model call bound to handle is about to be sent, with the
client's estimate of its size. No-op here.
note_call_usage(handle, input_tokens, output_tokens, cached_tokens=0, has_images=False)
classmethod
¶
One model call's server-reported usage, pushed by the chat client
bound to handle. No-op here.
Source code in backend/src/engines/base_engine.py
note_stream_abandoned(handle)
classmethod
¶
A model stream against handle (the child it was built for) did
not end normally. No-op here.
on_history_rewritten()
classmethod
¶
The conversation history was rewritten (compaction): the cached prefix is garbage. No-op here.
start_cleanup_task()
classmethod
¶
Start the automatic cleanup monitoring task.
Creates an async task that periodically checks for idle models and frees memory. Should be called once during application startup.
Note
Idempotent - calling multiple times has no effect if task already running.
Examples:
from src.engines.base_engine import BaseEngine engine = BaseEngine.get_engine() engine.start_cleanup_task()
Source code in backend/src/engines/base_engine.py
start_pressure_task()
classmethod
¶
stop_cleanup_task()
classmethod
¶
Stop the automatic cleanup monitoring task.
Cancels the cleanup monitor task. Should be called during application shutdown to gracefully stop background tasks.
Note
Idempotent - calling when no task is running has no effect.
Examples:
from src.engines.base_engine import BaseEngine engine = BaseEngine.get_engine() engine.stop_cleanup_task()
Source code in backend/src/engines/base_engine.py
stop_pressure_task()
async
classmethod
¶
take_idle_reload(llm_id)
classmethod
¶
Whether this turn reloads llm_id after an idle-timeout reap.
True once (the record is consumed) when the reap unloaded llm_id
and nothing is loaded. A record found while a model is loaded is stale
(the title request of a new conversation reloaded it first) and is
dropped. Identity is llm.id, as in _should_not_reload_model.
Source code in backend/src/engines/base_engine.py
warm_up_accelerator(duration_seconds=1.0)
abstractmethod
classmethod
¶
Warm up the hardware accelerator (GPU/Neural Engine) for optimal performance.
Runs compute-intensive operations to bring the accelerator to optimal performance state before benchmarking or inference. Implementation varies: - MLX: Matrix operations on MPS device - CUDA: CUDA kernel warm-up on GPU - CPU: CPU cache warm-up (minimal effect)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
duration_seconds
|
float
|
How long to run warm-up operations (default: 1.0). |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if warm-up completed successfully, False otherwise. |
Raises:
| Type | Description |
|---|---|
EngineException
|
If warm-up fails critically (rare, usually returns False). |
Note
This is particularly important for GPUs that dynamically adjust clocks. MLX/MPS benefits significantly from warm-up due to power management. CPU backend may implement minimal warm-up or skip entirely.
Examples:
>>> engine = BaseEngine.get_engine()
>>> success = engine.warm_up_accelerator(1.5)
>>> if success:
... print("Accelerator ready for benchmarking")
Source code in backend/src/engines/base_engine.py
EngineMeta
¶
Bases: ABCMeta
Metaclass for Engine classes with custom repr.
__repr__()
¶
Return human-readable engine class name.
Returns:
| Type | Description |
|---|---|
|
String representation like "LLM Engine: MLX_Engine". |
drain_pending_reset()
async
¶
Wait for an in-flight reset an earlier lock holder left behind.
Called right after acquiring the generation lock, before the new holder
does anything, and by the shutdown before it stops the child
(src.core.api.lifespan). The reset task's outcome is never re-raised into this
unrelated holder; a cancellation of the holder during the wait is
re-raised once the reset thread has finished.
Source code in backend/src/engines/base_engine.py
run_reset_shielded(fn)
async
¶
Run the blocking reset fn in a worker thread, shielded.
Registers the thread's task as BaseEngine._pending_reset for the next
lock acquirer to drain, waits for it through wait_shielded (a
cancellation never returns early), then re-raises a cancellation that
arrived meanwhile. Returns fn's result, or None when it raised
(logged once by the done callback). Every prefix-cache reset path -- the
claim of a conversation, the compaction reset and the memory brake's
samples (brake_point: MLX may reset the cache or move the child's
bound, the CPU engine may restart its child) -- goes through here.
Source code in backend/src/engines/base_engine.py
wait_shielded(task)
async
¶
Wait until task is done, whatever cancellation arrives meanwhile.
Returns whether a native cancellation of the CURRENT task was absorbed,
so the caller can re-raise it once the wait is over. The task's own
outcome is left to the caller (for a reset, _log_reset_outcome owns
it). Also used by src.agents.isolated_stream to wait for the task
that consumes the agent's LangGraph stream.
Source code in backend/src/engines/base_engine.py
src.engines.cpu_engine
¶
CPU engine for fallback inference using llama.cpp HTTP server.
This module provides a CPU-only backend for systems without GPU acceleration: - Uses llama.cpp server (OpenAI-compatible API) for optimized CPU inference - GGUF model format for quantization (4-bit, 5-bit, 8-bit) - Threading optimization for multi-core CPUs - Fallback when MLX (Mac Silicon) or CUDA (NVIDIA GPU) unavailable - Rosetta support for x86_64 binaries on Apple Silicon
Current Status
FUNCTIONAL - Core features implemented. Performance evaluation pending refinement.
Features
- Load GGUF quantized models via llama-server subprocess
- Multi-threaded inference with configurable thread count
- Context window sized by Erudi at spawn (-c = min(trained window, what 0.75 x RAM affords), see gguf_memory); llama-server's own fit when the GGUF header is unreadable; ERUDI_CTX pins it
- A memory brake: under memory pressure the child respawns with a smaller window, or is unloaded (memory_brake.llama_step, memory_pressure)
- Streaming generation via OpenAI-compatible /v1/chat/completions
- Automatic port selection and server lifecycle management
- Cross-platform (macOS, Linux, Windows) with Rosetta support
Architecture
CPU Engine (Singleton): ┌───────────────────────────────────────────────────────────┐ │ get_model_and_tokenizer() │ │ └─> Start llama-server → return handle │ └───────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────┐ │ token streaming lives in the agent layer, not the engine: │ │ AgentRunner → ChatOpenAI(base_url) → POST /v1/chat/... │ │ the engine only spawns / probes / reaps the server │ └───────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────┐ │ cleanup() │ │ └─> Terminate server process → free memory │ └───────────────────────────────────────────────────────────┘
Example
::
from src.engines.cpu_engine import CPU_Engine
model, tokenizer = CPU_Engine.get_model_and_tokenizer(
llm_id="mistral-7b-q4",
llm_local_path="/path/to/model.gguf"
)
# Token streaming is driven by the agent layer (ChatOpenAI(base_url=...)),
# not the engine; ``model["base_url"]`` is what it connects to.
Note
BaseEngine.get_engine() will automatically select CPU_Engine if: - System is not Mac Silicon (no MLX) - No CUDA-capable GPU detected (no CUDA_Engine)
This ensures graceful degradation for unsupported hardware.
Warning
CPU inference is significantly slower than GPU (10-50x depending on model size). Use only as last resort fallback when GPU acceleration is unavailable. For production deployments, prefer MLX (Mac) or CUDA (NVIDIA) engines.
CPU_Engine()
¶
Bases: BaseLlamaCppEngine
CPU Engine using llama.cpp's HTTP server (llama-server).
Inherits the subprocess + SSE lifecycle from BaseChatServerEngine and the
llama-cpp specifics (Popen, GGUF picking, llama-server location) from
BaseLlamaCppEngine. CPU-specific here: _build_spawn_argv, the window
it sizes itself (_prepare_spawn_context), the memory brake's bindings,
and class attrs.
Notes
- "model" is a dict handle for the running server (pid, port, alias, base_url)
- "tokenizer" is a placeholder (HTTP server abstracts tokenization)
Source code in backend/src/engines/base_engine.py
brake_point(point, *, llm_id=None)
classmethod
¶
Take the brake's sample at point and execute its decision.
Never raises: a failure is ONE WARNING with the exception's class
name, and the sample is lost.
Source code in backend/src/engines/cpu_engine.py
brake_reload_pending(llm_id)
classmethod
¶
An unload's record for llm_id (the shared answer), or a brake
respawn of its loaded child whose reload state is still owed.
Source code in backend/src/engines/cpu_engine.py
get_flat_hardware_data()
classmethod
¶
Get hardware data in flat format compatible with HardwareProfile entity.
Returns hardware specifications as a flat dictionary ready for database insertion. For CPU backend, get_performance_evaluation() already returns data in the correct flat format.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Flat dict with all fields matching HardwareProfile columns. |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If hardware data collection fails. |
Source code in backend/src/engines/cpu_engine.py
get_hardware_info()
classmethod
¶
Get comprehensive hardware information for CPU-only backend.
Returns detailed hardware specifications including CPU model, system memory, storage capacity, and platform information. No GPU/accelerator data.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing hardware specifications following BaseEngine contract: |
Dict[str, Any]
|
{ "system": {"platform": str, "platform_version": str, ...}, "cpu": {"model": str, "total_cores": int, "architecture": str, ...}, "memory": {"total_memory_gb": float, "available_memory_gb": float, ...}, "storage": {"total_gb": float, "available_gb": float, "usage_percentage": float}, "backend_type": "cpu", "timestamp": float |
Dict[str, Any]
|
} |
Note
GPU fields return None/False since this is CPU-only backend. Falls back gracefully if psutil unavailable.
Source code in backend/src/engines/cpu_engine.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | |
get_performance_evaluation()
classmethod
¶
Calculate comprehensive performance metrics for CPU-only backend.
Evaluates hardware capabilities and returns performance scores for inference workloads. Scoring acknowledges CPU limitations compared to GPU-accelerated backends.
Scoring methodology
- Inference: CPU cores (40%), memory capacity (30%), memory bandwidth est. (20%), disk (10%)
Normalization
- CPU cores: 64 cores = 100 points
- Memory: 128GB = 100 points
- Memory bandwidth: Estimated from CPU specs, 100GB/s = 100 points
- Disk: 500GB available = 100 points
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing performance metrics and scores (0-100 scale): |
Dict[str, Any]
|
{ "backend_type": "cpu", "cpu_model": str, "total_memory_gb": float, "available_memory_gb": float, "compute_units": int, # CPU cores "cpu_performance_units": int, "global_inference_score": float, "global_inference_label": str, "cpu_score": float, "memory_score": float, "performance_breakdown": PerformanceBreakdown, ... |
Dict[str, Any]
|
} |
Note
CPU backend scores are generally lower than GPU backends due to lack of parallel processing capabilities. Scores are calibrated to be realistic about CPU limitations while still differentiating between high-end and low-end CPU systems.
Source code in backend/src/engines/cpu_engine.py
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 | |
warm_up_accelerator(duration_seconds=1.0)
classmethod
¶
Warm up CPU with compute workload.
Runs lightweight computation loop to bring CPU to stable performance state. Less critical than GPU warm-up but still useful for consistent benchmarking results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
duration_seconds
|
float
|
How long to run warm-up operations (default: 1.0). |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if warm-up completed successfully, False otherwise. |
Note
CPU warm-up is minimal compared to GPU since CPUs don't have dynamic clock management like modern GPUs.
Source code in backend/src/engines/cpu_engine.py
src.engines.cuda_engine
¶
CUDA engine for NVIDIA GPU inference.
This module implements the CUDA backend for local LLM inference on systems with NVIDIA GPUs. It provides: - Hardware detection via PyTorch CUDA and NVML - Performance evaluation for inference - GPU warm-up for optimal benchmarking - Cross-platform support (Windows and Linux)
Architecture
CUDA Engine → PyTorch CUDA + NVML ┌───────────────────────────────────────────────────────────┐ │ get_hardware_info() │ │ └─> Detect GPU, VRAM, compute capability via NVML │ └───────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────┐ │ get_performance_evaluation() │ │ 1. Calculate TFLOPS (FP32, FP16, BF16) │ │ 2. Measure memory bandwidth │ │ 3. Score inference performance │ └───────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────┐ │ warm_up_accelerator() │ │ └─> Matrix operations to boost GPU clocks │ └───────────────────────────────────────────────────────────┘
Hardware Detection
- CUDA availability: torch.cuda.is_available()
- GPU information: NVML (pynvml library)
- Best GPU selection: highest VRAM capacity
- OS-agnostic: Works on Windows and Linux
Performance Scoring
Inference weights: - GPU compute (40%): Tensor TFLOPS (BF16/FP16) - Memory bandwidth (30%): GB/s throughput - VRAM capacity (15%): GPU memory size - CPU performance (5%): Multi-core GHz - System RAM (5%): Host memory - PCIe bandwidth (5%): Bus capacity
Example
Get CUDA hardware information::
from src.engines.cuda_engine import CUDA_Engine
# Detect hardware
hw_info = CUDA_Engine.get_hardware_info()
print(f"GPU: {hw_info['gpu']['gpu_name']}")
print(f"VRAM: {hw_info['gpu']['vram_total_gb']} GB")
# Warm up GPU
CUDA_Engine.warm_up_accelerator(1.5)
# Evaluate performance
perf = CUDA_Engine.get_performance_evaluation()
print(f"Inference score: {perf['global_inference_score']}/100")
print(f"Label: {perf['global_inference_label']}")
Inference (llama.cpp compiled with CUDA, a llama-server subprocess): - The window (-c) is sized by Erudi at spawn for a FULL offload, against the selected GPU's memory at load (cuda_memory); llama.cpp's fit places the layers (-ngl -1, "auto" while fit is on) with Erudi's margin (-fitt). When the floor window does not fit or the GGUF cannot be modelled, no -c: fit sizes the window and places the layers. - After load the placement is classified (full or partial offload); on Windows a full-offload child carries a memory brake driven by the WDDM spill gauge alone -- its Shared GPU memory above its baseline, the reading at load bounded by the healthy level of its window (memory_brake.llama_step); RAM pressure never drives it. On Linux, which has no spill, the brake has no signal and never acts. A partial or fit-sized child runs brake-off. - One GPU, pinned by UUID (CUDA_VISIBLE_DEVICES), never over a value the user set.
Note
- Requires torch with CUDA support
- Requires pynvml for GPU monitoring and the window sizing
- All methods return fallback values if CUDA unavailable
- Thread-safe through BaseEngine._lock
CUDA_Engine()
¶
Bases: BaseLlamaCppEngine
Engine for NVIDIA CUDA GPU inference.
Provides hardware detection and performance evaluation for NVIDIA GPUs using PyTorch CUDA backend and NVML monitoring library.
Hardware Specs Reference Tables
CUDA_PER_SM: CUDA cores and Tensor cores per SM by compute capability TC_OPS: Tensor core operations per precision by compute capability
Note
Inference uses a CUDA-compiled llama-server subprocess (same architecture as CPU_Engine). The window is sized for a full offload against the GPU's memory; llama.cpp's fit places the layers.
Source code in backend/src/engines/base_engine.py
brake_point(point, *, llm_id=None)
classmethod
¶
Take the brake's sample at point and execute its decision --
on Windows only, and only on a child whose window Erudi sized and
that loaded in full: a fit-sized or partial child is never sampled
(nothing for the law's unload to act on either). Off Windows there
is no spill, so no signal: nothing is read and nothing acts (the idle
monitor still ticks; its samples return here). Never raises: a
failure is ONE WARNING with the exception's class name, and the
sample is lost.
Source code in backend/src/engines/cuda_engine.py
brake_reload_pending(llm_id)
classmethod
¶
An unload's record for llm_id (the shared answer), or a brake
respawn of its loaded child whose reload state is still owed.
Source code in backend/src/engines/cuda_engine.py
get_flat_hardware_data()
classmethod
¶
Get hardware data in flat format compatible with HardwareProfile entity.
Calls get_performance_evaluation() and strips keys that have no matching column in the HardwareProfile ORM model so the result can be splatted directly into the entity constructor.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Flat dict with fields matching HardwareProfile columns. |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If hardware data collection fails. |
Source code in backend/src/engines/cuda_engine.py
get_hardware_info()
classmethod
¶
Get comprehensive hardware information for NVIDIA CUDA backend.
Returns detailed hardware specifications including GPU model, VRAM, compute capability, system resources, and CUDA availability.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing hardware specifications: |
Dict[str, Any]
|
{ "system": { "platform": str, "platform_version": str, "machine": str, "processor": str }, "cpu": { "model": str, "architecture": str, "total_cores": int, "logical_cores": int }, "memory": { "total_memory_gb": float, "available_memory_gb": float, "memory_pressure": float, "memory_type": "system" }, "gpu": { "gpu_name": str, "gpu_index": int, "cuda_cores": int, "vram_total_gb": float, "vram_available_gb": float, "compute_capability": str, "memory_bandwidth_gbs": float, "cuda_available": bool, "unified_memory": False }, "storage": { "total_gb": float, "available_gb": float, "usage_percentage": float }, "backend_type": "cuda", "timestamp": float |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If critical hardware detection fails. |
Note
Returns fallback values if CUDA unavailable rather than raising. Safe to call even on systems without NVIDIA GPUs.
Examples:
>>> hw_info = CUDA_Engine.get_hardware_info()
>>> print(f"GPU: {hw_info['gpu']['gpu_name']}")
>>> print(f"VRAM: {hw_info['gpu']['vram_total_gb']} GB")
>>> print(f"Compute: {hw_info['gpu']['compute_capability']}")
Source code in backend/src/engines/cuda_engine.py
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 | |
get_performance_evaluation()
classmethod
¶
Calculate comprehensive performance metrics for NVIDIA CUDA backend.
Evaluates hardware capabilities and returns normalized performance scores for inference workloads. Scoring based on TFLOPS, memory bandwidth, VRAM capacity, and system resources.
Scoring methodology
Inference (0-100 scale): - GPU compute (40%): Tensor TFLOPS (BF16/FP16) - Memory bandwidth (30%): GB/s throughput - VRAM capacity (15%): GPU memory size - CPU performance (5%): Multi-core units - System RAM (5%): Host memory - PCIe capacity (5%): Bus bandwidth
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing performance metrics and scores: |
Dict[str, Any]
|
{ "backend_type": "cuda", "gpu_name": str, "cpu_model": str, "total_memory_gb": float, "available_memory_gb": float, "memory_bandwidth_gbs": float, "disk_total_gb": float, "disk_available_gb": float, "estimated_tflops": float, "compute_units": int, "cpu_performance_units": float, "cuda_version": str, "compute_capability": str, "architecture": str, "global_inference_score": float, "global_inference_label": str, "gpu_score": float, "cpu_score": float, "memory_score": float, "unified_memory": False, "accelerator_available": bool, "system_platform": str, "performance_breakdown": dict |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If evaluation fails critically. |
Note
Returns fallback scores if CUDA unavailable. Should call warm_up_accelerator() before evaluation for accuracy.
Examples:
>>> CUDA_Engine.warm_up_accelerator(1.5)
>>> eval_result = CUDA_Engine.get_performance_evaluation()
>>> print(f"Inference: {eval_result['global_inference_score']}/100")
>>> print(f"Label: {eval_result['global_inference_label']}")
Source code in backend/src/engines/cuda_engine.py
1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 | |
warm_up_accelerator(duration_seconds=1.0)
classmethod
¶
Warm up NVIDIA GPU to boost clock speeds for benchmarking.
Runs matrix multiplication operations on GPU to bring it to optimal performance state before measurements. Important for GPUs with dynamic clock management.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
duration_seconds
|
float
|
How long to run warm-up operations (default: 1.0). |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if warm-up completed successfully, False otherwise. |
Note
Silently returns False if CUDA unavailable rather than raising. Uses 4096x4096 matrix multiplications on primary GPU.
Examples:
>>> success = CUDA_Engine.warm_up_accelerator(1.5)
>>> if success:
... print("GPU ready for benchmarking")
Source code in backend/src/engines/cuda_engine.py
src.engines.cuda_compatibility
¶
Can this NVIDIA GPU actually run the CUDA llama-server Erudi ships?
BaseEngine.get_engine() picks CUDA_Engine as soon as NVML reports one
NVIDIA device. That is the right default, but it is not sufficient: the CUDA
build we ship targets a fixed architecture list, so a card below it -- or a
driver too old to JIT the PTX in it -- crashes on the first chat message with a
raw GGML_ABORT instead of running on the CPU it would run on perfectly well.
This module holds the two halves of the answer, both free of NVML imports at module scope so it stays importable everywhere:
- :func:
cuda_preflight_notice-- two NVML reads at startup (compute capability, CUDA driver version), turned into anengine_noticepayload the frontend renders as a decision. It never raises. - :func:
classify_cuda_failure-- the runtime half: the tail a dead child printed, matched against the fixed strings ggml emits, so the chat stream's error event carries a code instead of a wall of text.
Neither half ever switches the engine on its own: the CPU fallback is
PROPOSED, and only the persisted user_settings.inference_backend
preference (or a reinstall with the CPU build) applies it.
classify_cuda_failure(child_output)
¶
Map a dead child's captured output to a CUDA failure code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
child_output
|
Optional[str]
|
The tail the drainer collected before the child exited.
|
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
One of :data: |
Optional[str]
|
CUDA failure at all -- a missing GGUF, a corrupt model, a port clash. |
Optional[str]
|
|
Source code in backend/src/engines/cuda_compatibility.py
cuda_preflight_notice()
¶
Verdict on the selected NVIDIA GPU, as an engine_notice payload.
Reads the first NVML device's compute capability and the system's CUDA
driver version through CUDA_Engine's existing helpers, and returns:
CUDA_COMPUTE_CAPABILITY_TOO_LOWwhen the card is below CUDA 12's own floor -- there is no driver that fixes it;CUDA_DRIVER_TOO_OLDwhen the card is fine but the driver is below the version it needs (see :func:required_cuda_version);Nonewhen the machine can run the bundled build, when a reading is unavailable, or when NVML fails.
An unreadable value yields None on purpose: guessing from a failed read
is how a healthy card gets told its driver is too old. Never raises --
startup must not fail because a diagnostic could not run.
Source code in backend/src/engines/cuda_compatibility.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
format_cuda_version(version)
¶
Render an NVML CUDA version int (12080) for display ("12.8").
required_cuda_version(capability)
¶
The driver's CUDA version this card needs to run the bundled binary.
A card with native SASS in the binary only needs the runtime baseline; a card that depends on the PTX needs the toolkit's own driver family.
Source code in backend/src/engines/cuda_compatibility.py
src.engines.mlx_engine
¶
MLX engine for Apple Silicon inference via mlx_vlm.server subprocess.
Inference is delegated to an out-of-process mlx_vlm.server HTTP server
(OpenAI-compatible), spawned by multiprocessing.Process and reached over
loopback HTTP — exactly aligned with the CPU/CUDA pattern that wraps the
llama-server binary. mlx-vlm is a superset of mlx-lm (it depends on it): it
serves plain text models, carries a working tool-calling parser (no
mlx_lm.server EOS-flush drop, so agentic tools fire), and accepts image input.
Apple Silicon hardware detection remains in-process: it doesn't need the
server and would only inflate cold-start time if it did.
Architecture
MLX_Engine (singleton) ┌───────────────────────────────────────────────────────────────┐ │ get_model_and_tokenizer(llm_id, path) │ │ 1. Pick free TCP port (27300+) │ │ 2. Spawn child: mp.Process(target=run_mlx_vlm_server) │ │ 3. Poll GET /health until 200 (≤120s) │ │ 4. atexit.register(terminate) │ │ 5. Cache handle {pid,proc,port,base_url,alias,model_path} │ └───────────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────────┐ │ token streaming lives in the agent layer, not the engine: │ │ AgentRunner → Erudi_Chat_OpenAI(base_url) → POST /v1/... │ │ carries delta.content AND delta.reasoning (#554) │ └───────────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────────────────────────┐ │ cleanup() — override │ │ └─> SIGTERM child → join(5s) → SIGKILL if needed │ │ → wait_port_closed → super().cleanup() │ └───────────────────────────────────────────────────────────────┘
The memory brake (brake_point, see the section below) gives memory back
under macOS memory pressure, at turn boundaries and from an idle monitor.
Where the child's output goes:
An mp.Process has no output pipe, so the child redirects its own stdout
and stderr into logs/mlx-child-<port>.log (mlx_child_log, wired in
_mlx_vlm_server_runner). _read_child_output quotes the tail of that
file in every crash report and probe timeout, exactly where the llama-cpp
engines quote their drainer.
Why multiprocessing instead of subprocess.Popen([sys.executable, "-m", ...])?
In a PyInstaller frozen build, sys.executable is the launcher binary,
not a Python interpreter, so the -m flag is a no-op. mp.spawn
(already configured in backend/run.py:143-160 via mp.freeze_support()
+ set_start_method("spawn", force=True)) re-executes the binary in
child mode and reconstitutes the import graph — the same run_mlx_vlm_server
target works in dev (real Python) and in prod (frozen).
Why the <|channel>thought ... <channel|> manual filter is gone:
mlx-vlm's native server-side split owns the reasoning markers (#554): the
child spawns with --enable-thinking (thinking on by default) and streams
chain-of-thought in the dedicated delta.reasoning field (mirrored into
delta.reasoning_content on the pinned 0.6.17), which Erudi_Chat_OpenAI
re-attaches to the message chunks and the runner emits as thinking
events — symmetric to llama-server's default --reasoning-format auto on
the CPU/CUDA path. The runner's ThinkSplitter stays as the inline fallback
for families whose markers the server split does not know.
Why the _MLX_EXECUTOR thread bottleneck is gone:
Generation now runs in a separate OS process; the GPU stream is
initialised inside that child, fully isolated from the FastAPI parent.
The Stream(gpu, 0) crash that motivated the persistent thread executor
(commits cefdc7a, 40fb55e) is structurally impossible from the parent.
Model format
The engine only ever loads pre-built MLX repos (HF library tag mlx,
see FORMAT_TAG): the catalog and the HF search are filtered on that tag
and a by-link download is refused up front when the repo lacks it. There is
no local HF -> MLX conversion (#408).
Example
::
from src.engines.mlx_engine import MLX_Engine
model, tokenizer = MLX_Engine.get_model_and_tokenizer(
llm_id="mistral-7b",
llm_local_path="/path/to/mlx/model"
)
# Token streaming is driven by the agent layer (ChatOpenAI(base_url=...)),
# not the engine; ``model["base_url"]`` is what it connects to.
MLX_Engine.cleanup()
Warning
Only use on Apple Silicon. On other platforms, BaseEngine.get_engine() will select CUDA_Engine or CPU_Engine instead.
MLX_Engine()
¶
Bases: BaseChatServerEngine
Singleton Engine for MLX models and tokenizers runtimes. Built for Apple Silicon Backends.
Source code in backend/src/engines/base_engine.py
begin_memory_window()
classmethod
¶
Open the turn's measurement window on the loaded child; returns the
(handle, pid, window) token end_memory_window closes it with
(window numbers the turn's window on this child: a token of an
earlier window never closes a later one).
Source code in backend/src/engines/mlx_engine.py
brake_point(point, *, llm_id=None)
classmethod
¶
Take the brake's sample at point and execute its decision.
turn_start samples only when the loaded child is the one the turn
for llm_id will use. Never raises: a failure is ONE WARNING with
the exception's class name, and the sample is lost.
Source code in backend/src/engines/mlx_engine.py
claim_prefix(owner)
classmethod
¶
Make the prefix cache belong to owner, resetting it if needed.
Same owner: nothing to do. A fresh child: its pool is empty, so the
owner is recorded without a reset. Anything else (another owner, a
previous reset that failed -- <dirty> -- or no owner at all): reset,
then record the owner. A failed reset leaves the owner <dirty> so
the next claim retries; it never fails the turn.
Source code in backend/src/engines/mlx_engine.py
compute_wire_tools(llm_local_path)
classmethod
¶
Verified tool-call wire capability on the mlx-vlm server (#298).
Runs mlx-vlm's OWN parser inference (_infer_tool_parser) on the
model's chat template: the server picks its tool parser from exactly
that function at load time, so this executes the same code the server
executes — the verdict is exact by construction for the pinned mlx-vlm
(0.6.13). A template matching no parser entry means the server streams
the model's tool-call output as raw text (#295), hence wire False.
Tokenizer-level only (_load_capability_tokenizer: local files, no
weights, no server). Any failure to load mlx-vlm or the tokenizer
returns None (unverified) rather than a wrong verdict.
Source code in backend/src/engines/mlx_engine.py
end_memory_window(token, *, abandoned=False, compaction_threshold=None)
classmethod
¶
Close the window and record the turn's observation; returns it, or
None when nothing could be recorded.
Recorded only on a MEASURED base (never the weights-on-disk fallback)
and a window that really started. A child that died during the window
is recorded too, child_died and no peak: it is the ground truth of
an under-prediction. An ABANDONED turn whose call was cut before its
usage arrived is recorded with n_est (the client's estimate of the
call in flight) and no measured n; an estimate never raises the
exceeded-prediction WARNING.
Whatever is recorded, the turn's largest request is written to
handle["last_request_tokens"] first: the measured n, else the
estimate of the call in flight (n_est: an abandoned turn, or one
that recovered from an error before any usage arrived, such as a
first-chunk timeout); a turn with neither keeps the previous value.
compaction_threshold -- the threshold of the conversation that
sent it, None for an arena turn -- is written beside it, to
handle["last_request_threshold"], and only then: the pair stays
the request of one turn and the trigger it is judged against.
The memory brake shrinks the window only when the next turn would
compact under it, and a missing SPI or base must not hide that.
Source code in backend/src/engines/mlx_engine.py
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 | |
get_flat_hardware_data()
classmethod
¶
Get hardware data in flat format compatible with HardwareProfile entity.
Returns hardware specifications as a flat dictionary ready for database insertion. For MLX backend, get_performance_evaluation() already returns data in the correct flat format.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Flat dict with all fields matching HardwareProfile columns. |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If hardware data collection fails. |
Source code in backend/src/engines/mlx_engine.py
get_hardware_info()
classmethod
¶
Get comprehensive hardware information for Apple Silicon.
Returns detailed hardware specifications including Apple chip model, unified memory, MPS availability, and Neural Engine specs.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing hardware specifications following BaseEngine contract: |
Dict[str, Any]
|
{ "system": {"platform": "Darwin", ...}, "cpu": {"model": str, "is_apple_silicon": True, ...}, "memory": {"total_memory_gb": float, "memory_type": "unified", ...}, "gpu": {"gpu_name": str, "mlx_gpu_cores": int, "unified_memory": True, ...}, "accelerator": {"neural_engine_tops": float, "architecture": str}, "storage": {"total_gb": float, "available_gb": float}, "backend_type": "mlx", "timestamp": float |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If critical hardware detection fails. |
Note
Returns fallback values on non-critical failures rather than raising.
Examples:
>>> hw_info = MLX_Engine.get_hardware_info()
>>> print(f"Chip: {hw_info['cpu']['model']}")
>>> print(f"GPU Cores: {hw_info['gpu']['mlx_gpu_cores']}")
Source code in backend/src/engines/mlx_engine.py
1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 | |
get_performance_evaluation()
classmethod
¶
Calculate comprehensive performance metrics for Apple Silicon.
Evaluates hardware capabilities and returns performance scores for inference workloads. Scoring optimized for Apple Silicon unified memory architecture.
Scoring methodology
- Inference: GPU compute (35%), memory bandwidth (30%), memory (20%), Neural Engine (10%), CPU (5%)
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict containing performance metrics and scores (0-100 scale): |
Dict[str, Any]
|
{ "backend_type": "mlx", "gpu_name": str, "cpu_model": str, "total_memory_gb": float, "available_memory_gb": float, "memory_bandwidth_gbs": float, "disk_total_gb": float, "disk_available_gb": float, "estimated_tflops": float, "mlx_gpu_cores": int, "cpu_performance_units": float, "neural_engine_tops": float, "architecture": str, "global_inference_score": float, "global_inference_label": str, "gpu_score": float, "cpu_score": float, "memory_score": float, "unified_memory": True, "mps_available": bool, "system_platform": "Darwin", "mlx_chip_model": str, "performance_breakdown": dict |
Dict[str, Any]
|
} |
Raises:
| Type | Description |
|---|---|
HardwareException
|
If evaluation fails critically. |
Examples:
>>> eval_result = MLX_Engine.get_performance_evaluation()
>>> print(f"Inference: {eval_result['global_inference_score']}/100")
>>> print(f"Label: {eval_result['global_inference_label']}")
Source code in backend/src/engines/mlx_engine.py
2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 | |
max_recommended_working_set_bytes()
classmethod
¶
The GPU's usable working set in bytes, or None if unavailable.
On Apple Silicon Metal caps the GPU at a recommended working set well
below total unified memory (~74 % of RAM; mlx-vlm pins the wired limit
to it on every generation). That working set -- not total RAM -- is the
hard ceiling the memory accounting plans against, so
memory_budget.total_memory_bytes reads it here (#601), and the
recorded memory observations carry it as part of their key.
Kept deliberately off get_flat_hardware_data /
get_performance_evaluation: this fact does not belong in the
persisted HardwareProfile schema; a dedicated method keeps the hardware
knowledge in the engine without polluting the profile.
The mlx import is lazy (nothing at module import time may require
mlx) and any failure answers None -- this may be called on a host
where the query is unavailable.
Source code in backend/src/engines/mlx_engine.py
memory_bound_tokens()
classmethod
¶
The memory bound applied to the loaded child, or None.
Source code in backend/src/engines/mlx_engine.py
model_supports_vision(llm_local_path)
classmethod
¶
An MLX model is vision-capable iff its config.json declares vision.
Reads the directory's config.json only (no weights, no server) and
defers the decision to config_declares_vision. Unreadable/absent
config -> None (permissive).
Source code in backend/src/engines/mlx_engine.py
note_call_start(handle, estimated_prompt_tokens)
classmethod
¶
A call is in flight: its estimated size, until its usage arrives.
Source code in backend/src/engines/mlx_engine.py
note_call_usage(handle, input_tokens, output_tokens, cached_tokens=0, has_images=False)
classmethod
¶
One call's usage, with the window's sequence number at push time.
Source code in backend/src/engines/mlx_engine.py
note_stream_abandoned(handle)
classmethod
¶
Flag handle -- the child the stream was BUILT against, never the
one loaded now -- so its next reset sends a barrier first.
Source code in backend/src/engines/mlx_engine.py
on_history_rewritten()
classmethod
¶
Compaction rewrote the history: the cached prefix is garbage, and the turn's memory measurement restarts (the summary call's peak and the hops before it never count).
Source code in backend/src/engines/mlx_engine.py
validate_local_artifact(llm_local_path)
classmethod
¶
Integrity gate for an MLX snapshot (#88).
An MLX model is loadable iff its directory ships config.json + a
tokenizer file + at least one weights file, each non-empty. Raises
:class:EngineException with a curated, user-facing message on the
first missing/empty essential. Backs both the download-completion gate
and the pre-spawn load gate.
Source code in backend/src/engines/mlx_engine.py
warm_up_accelerator(duration_seconds=1.0)
classmethod
¶
Warm up Apple Silicon GPU using Metal Performance Shaders.
Runs matrix operations on MPS device to bring GPU to optimal performance state before benchmarking or inference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
duration_seconds
|
float
|
How long to run warm-up operations (default: 1.0). |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if warm-up completed successfully, False otherwise. |
Note
Particularly important for Apple Silicon due to dynamic clock management.
Examples: