Architecture Overview¶
Table of Contents¶
- What mlxcel Is
- High-Level Runtime Diagram
- Control Plane vs Data Plane
- Key Design Principles
- Zero Python Dependency
- Config-Driven Model Detection
- Enum-Based Exhaustive Dispatch
- The LanguageModel Trait
- Modular VLM Pipeline
- Module Map
- Two Binaries
- Environment Configuration
What mlxcel Is¶
mlxcel is a high-performance, general-purpose inference engine written in Rust. Its MLX backend runs generation, multimodal understanding, audio (including Nemotron VoiceChat full-duplex speech), detection, embeddings, and reranking on Apple Silicon through Metal, on supported NVIDIA GPUs through CUDA, and experimentally on AMD GPUs through ROCm; no Python runtime sits in the inference request path.
The key architectural commitment is a direct binding from Rust to MLX C++, bypassing Python entirely. Weight loading, forward passes, sampling, pooling, normalization, ranking, and dedicated audio, detection, and diffusion runtimes all reach the native MLX computation graph through Rust APIs.
High-Level Runtime Diagram¶
The diagram below shows the common path from command or HTTP entry points through model-kind detection, task-specific workers, native tensor operations, and the MLX runtime. Generation uses a batch or sequential scheduler; embedding and reranking use dedicated MLX-owning workers; block diffusion, audio, sequence-to-sequence vision, and detection also use specialized runtimes.
CLI commands resolve a checkpoint, select a task, and invoke the matching forward path. The server can coordinate concurrent generation through its batch scheduler while an embedding worker and reranking worker separately own their model, tokenizer, and MLX arrays. These task paths share the same native MLX binding and common tensor primitives on Metal, CUDA, ROCm, or CPU.
Control Plane vs Data Plane¶
mlxcel draws a clear boundary between the control plane and the data plane.
The control plane lives in src/ (the main crate). It is responsible for:
- Reading
config.jsonto identify model architecture - Selecting and executing the correct loader
- Constructing the
LoadedModelenum wrapper - Loading and managing the tokenizer
- Applying chat templates (Jinja2 via minijinja)
- Installing the load-time model surgery pipeline when configured
- Assembling the
SamplingConfigfrom user-facing parameters - Routing VLM requests through image preprocessing and embedding projection
- Resolving embedding pooling, normalization, instructions, and length limits
- Constructing reranker pairs, multimodal items, and response envelopes
- Managing the HTTP server, generation scheduler, specialized worker queues, and request lifecycle
For autoregressive generation, the control plane does not implement tensor math itself. It drives the data plane through the LanguageModel trait and the decode Engine in mlxcel-core: mlxcel generate decodes on DirectEngine, a one-sequence client of Engine, and the server's BatchScheduler keeps admission, tick policy, preemption, and prompt-cache lookup while every forward runs through Engine. Embedding, reranking, audio, detection, diffusion, and sequence-to-sequence runtimes use their own typed model or worker interfaces.
The data plane lives in src/lib/mlxcel-core/. It is responsible for:
- Executing autoregressive model forward passes through
LanguageModel::forward() - Running every forward, sampler draw, and finish step through
Engine(open,prefill,step, the pipelinedsubmit/finish_rowspair, andclose); a single sequence is a step batch of one - Managing KV cache state through
KVCache,RotatingKVCache, andChunkedKVCache, which also choose dense or paged attention for the model (KVCache::attend,cache::attend_batched) - Planning prefill geometry with
PrefillPlanand sampling each row throughRowSampler, with token bias composed bycompose_token_bias - Bridging tensor operations to MLX C++ through the
cxxbridge declared insrc/lib/mlxcel-core/src/lib.rsand wrapped by modules such asops.rs - Running the speculative round loops: the engine's token-only loop (
Engine::verify_round) for the prompt-lookup and full-model drafters, and the MTP and DFlash drafters - Providing common attention-mask, pooling, normalization, projection, and layer operations used by embedding and ranking models
The data plane is tensor-compute focused and can execute on Metal, CUDA, ROCm, or CPU. mlxcel's own fused kernels live in src/lib/mlxcelverse/kernels/ and are selected by the resolved GPU backend; most have Metal, CUDA, and ROCm (HIP) ports, and a backend without a port of a given kernel takes the MLX graph fallback for that step. It has no knowledge of HTTP routing or chat-template policy; model wrappers still encode the architecture-specific tensor graph.
This separation keeps each concern testable in isolation. Sampling logic in sampling.rs can be tested with simple float tensors, the engine in engine/ with stub LanguageModel implementations, and control-plane routing in loading/mod.rs by mocking weight loading.
Key Design Principles¶
1. Zero Python Dependency¶
mlxcel uses cxx to bind directly to MLX C++ without a Python interpreter. The bridge declarations live in src/lib/mlxcel-core/src/lib.rs; Rust wrappers such as ops.rs, layers.rs, and sampling.rs expose typed APIs over the C++ operations used by model runtimes.
This eliminates the Python startup time, Python GIL contention, and Python object overhead. It also means the inference process can be a single native binary with no pip dependencies.
2. Config-Driven Model Detection¶
When you point mlxcel at a model directory, it reads config.json and related checkpoint metadata. get_model_type() in src/models/detection.rs maps known generation keys to ModelType, while architectures, sentence-transformers pooling layouts, bidirectional markers, and sequence-classification heads can select embedding or reranking kinds before ordinary generation dispatch.
The mapping is exhaustive for registered architecture variants: an unknown or contradictory layout fails with a clear error rather than silently choosing the wrong task or architecture.
Some model types require additional inspection. For example, hunyuan checks the num_experts field to distinguish the MoE and dense variants. Models with a vision_config field at the top level are automatically redirected to their VLM variant.
3. Enum-Based Exhaustive Dispatch¶
Loaded generative models are stored in the LoadedModel enum defined in src/loaded_model.rs, spanning local, distributed, text, VLM, SSM/RNN, and hybrid execution paths. Embedding, reranking, audio, and object-detection models use task-specific traits or runtime wrappers rather than forcing every model kind into the generative enum.
The LanguageModel trait is delegated to the inner model through a single delegate_language_model! macro, which generates an exhaustive match arm for every variant. This has two properties:
- Adding a new model variant without updating the dispatch macro causes a compile error, not a silent fallback to wrong behavior.
- The Rust compiler can verify that every trait method is covered for every variant.
The enum approach preserves family identity, which matters for capability dispatch (for example, deciding whether a VLM embedding path is needed).
4. LanguageModel Trait¶
The LanguageModel trait in mlxcel-core/src/generate.rs is the single interface between the control plane and data plane for text generation. It defines:
| Method | Purpose |
|---|---|
forward() | Standard single-sequence forward pass |
forward_with_embeddings() | VLM prefill with pre-computed vision embeddings |
forward_batched() | Batched decode for continuous batching |
embed_tokens() | Token embedding extraction for VLM merging |
make_caches() | Allocate per-layer KV caches for this model |
num_layers() | Layer count for cache initialization |
eos_token_ids() | Stop token IDs |
after_prefill() | Hook called after VLM prefill completes |
supports_batching() | Whether the model can participate in continuous batching |
Most families, including Mamba, Mamba2, Jamba, NemotronH, and RWKV7, return true from supports_batching(): their recurrent or model-owned state is keyed by SequenceId, so several sequences can share one batched forward pass. batched_decode_kind() reports BatchedDecodeKind::Native when the model runs that pass as one shared graph and PerRowFallback when it loops over rows; the server logs decode path: native batched forward or decode path: per-row fallback at startup. A model that returns false (for example the OCR, Florence-2, Nemotron-Parse, Nemotron VoiceChat, and diffusion text runtimes) is clamped to max_batch_size 1 and served one sequence at a time.
5. Modular VLM Pipeline¶
Vision-language models extend the text generation pipeline with three additional stages:
- Visual Encoder: An image backbone (SigLIP, CLIP, ViT, MobileNetV5, etc.) converts pixel data into visual feature tensors.
- Projection / Connector: A learned projection maps the visual feature space into the language model's embedding space.
- Embedding Merger: The projected visual tokens are inserted into the token embedding sequence at positions marked by image placeholder tokens.
The merged embedding sequence replaces the normal input_ids path during the prefill step. After the prefill, the VLM decode step is identical to standard text generation: the model processes one token at a time with the accumulated KV cache.
Each VLM family has its own image preprocessing pipeline and placeholder token convention. These are handled in family-specific modules under src/multimodal/ and src/vision/.
Module Map¶
| Directory | Responsibility |
|---|---|
src/main.rs | CLI entry point, argument parsing, command dispatch |
src/lib.rs | Library crate root, public re-exports |
src/models/ | Per-architecture model implementations and ModelType enum |
src/models/detection.rs | config.json to ModelType mapping |
src/loading/ | Model loading entry points and route selection |
src/loading/config_backed.rs | Standard config-backed loader |
src/loading/nonstandard.rs | Custom directory loaders |
src/loading/special.rs | Weight-driven loaders for unusual architectures |
src/loading/vlm.rs | VLM directory loaders |
src/http_tls.rs | Installs the process-wide ring rustls provider and builds every reqwest client (model downloads, media fetches). Server certificates are verified against the operating system trust store; MLXCEL_EXTRA_CA_CERTS adds roots on top |
src/loaded_model.rs | LoadedModel enum and LanguageModel delegation |
src/loaded_model_capabilities.rs | Capability helpers for VLM dispatch |
src/model_metadata.rs | Load policy registry, route selection logic |
src/backend/ | Compute-backend seam: which engine executes forward(). Production builds expose MLX; retained OpenXLA source is not exposed by the root Cargo manifest. See OpenXLA Backend Source Status. |
src/tokenizer/ | HuggingFace, SentencePiece, and Tiktoken tokenizer backends |
src/multimodal/ | VLM prompt assembly and image token injection |
src/vision/ | Image preprocessing, visual encoders, projection layers |
src/embeddings/ | Embedding traits, detection/loading support, pooling, normalization, limits, batching, and MaxSim |
src/rerank/ | Reranker traits, cross-encoder and generative scoring, pair construction, and batching |
src/execution/runtime.rs | Device selection and wired memory limit setup |
src/execution/sampling.rs | SamplingConfig assembly from user parameters |
src/lora/ | LoRA adapter loading and weight fusion |
src/surgery.rs | Process-wide load-time surgery pipeline integration |
src/server/ | HTTP server, generation scheduler, embedding/reranking workers, request routing, and wire schemas |
src/server/webui/ | Bundled administrative WebUI: embedded static assets, the /ui-api/v1 control plane, and its browser security checks. See WebUI |
src/distributed/ | Tensor parallelism, pipeline parallelism, disaggregated serving |
src/lib/mlxcel-core/ | Data plane: decode engine, KV cache, sampling, FFI |
src/lib/mlxcel-surgery/ | Runtime model surgery operations and YAML parsing |
src/lib/mlxcel-core/src/lib.rs | cxx bridge declarations and crate-level Rust bindings to MLX C++ |
src/lib/mlxcel-core/src/generate.rs | LanguageModel trait |
src/lib/mlxcel-core/src/engine/ | Engine (owns the model and CachePool; open, prefill, step, submit/finish_rows, close), direct.rs DirectEngine (raw-completion client used by generate, benchmarks, and the inference session), speculative.rs and verify_round.rs (token-only speculative loop shared by generate --prompt-lookup, --draft-model, and the server), rows.rs (per-row sample and finish) |
src/lib/mlxcel-core/src/prefill_plan.rs | PrefillPlan: adopted-prefix skip, history-boundary split, chunk pieces, and padded-prefill tiles |
src/lib/mlxcel-core/src/cache.rs | KVCache, RotatingKVCache, ChunkedKVCache, CachePool |
src/lib/mlxcel-core/src/cache/attend.rs | Attention dispatch owned by the cache: KvAttention, KVCache::attend, cache::attend_batched |
src/lib/mlxcel-core/src/sampling.rs | Token sampling and penalty logic |
src/lib/mlxcel-core/src/sampling_row_step.rs | RowSampler, the per-row sampling step the engine runs for server and CLI |
src/lib/mlxcel-core/src/sampling_token_bias.rs | compose_token_bias (logit_bias, language bias, suppression) |
src/lib/mlxcel-core/src/speculative/ | Token-only drafters (PromptLookupDrafter, FullModelDrafter) and the MTP round loop |
src/lib/mlxcelverse/ | MLX C++ build: CMakeLists.txt fetches the pinned MLX commit, overlays/{cpu,metal,cuda,rocm}/ replace upstream backend files before libmlx compiles, and kernels/ holds mlxcel's own fused kernels and launchers |
src/lib/mlxcel-core/src/drafter/ | DFlash-family (Qwen 3.5 DFlash, LFM2 DSpark, Muse Glimmer assistant, Laguna DFlash), Gemma 4 assistant, and Qwen 3.5 MTP drafter implementations |
Two Binaries¶
The mlxcel workspace produces two binaries.
mlxcel is the command-line tool. It supports these user-facing subcommands:
run: Run a positional model argument in interactive chat mode, or one-shot generation with-p. Repo-id and bare-name arguments auto-download on first use.generate(aliasgen): Run a generation pass for a prompt, or enter the same interactive REPL when the prompt is omitted. Accepts all model, generation, and sampling options.embed: Create text or image embeddings offline and print cosine or MaxSim comparisons.rerank: Score documents against a query offline and print their ranking.inspect: Print a pre-load memory budget for a local path, repo-id, or bare model name without loading tensors.detect: Run RT-DETRv2 object detection on an image and print text or JSON boxes.serve: Start the generation, embedding, reranking, audio, and utility HTTP server. Accepts server configuration and task-specific provider options.list(aliasls): List downloaded model snapshots in the local store, with--json/-q/-v/--sortoutput controls.arch(aliassupported): Print all supported model architectures grouped by family.download: Download a HuggingFace repository snapshot into the global mlxcel store by default, or to an exact directory with--local-dir.rm: Remove a downloaded model snapshot from the managed mlxcel store.tune: Profile supported kernel launch configurations and write an opt-in autotune cache.
mlxcel-server is a dedicated server binary with the same serve functionality but packaged for deployment contexts where the full mlxcel CLI is less relevant. It also exposes the shared download subcommand so server-only deployment images can fetch model snapshots without Python tooling.
Both binaries share all library code. The difference is only in the entry point and argument surface.
Environment Configuration¶
Two foundational runtime variables are summarized here. The complete inventory, including memory limits, scheduler, cache, speculative, embedding, reranking, media, and diagnostic controls, is in Environment Variables.
| Variable | Default | Values |
|---|---|---|
MLXCEL_DEVICE | gpu | gpu, metal, cpu |
MLXCEL_WIRED_LIMIT | GPU max memory | max, 0, none, NGB, NMB, bytes |
MLXCEL_DEVICE selects the compute backend. On Apple Silicon, gpu and metal are equivalent and route computation through MLX's Metal backend. Setting cpu forces all computation onto the CPU, which is much slower but useful for debugging.
MLXCEL_WIRED_LIMIT controls how much GPU memory MLX pins (wires) so it cannot be evicted. The default behavior matches Python mlx-lm: the limit is set to the total GPU memory size, ensuring model weights stay resident for large models. Setting 0 or none disables wiring entirely. Explicit values like 24GB allow fine-grained control on machines where multiple processes share GPU memory.