| Property | Value |
|---|---|
| Provider | Mistral AI |
| GGUF architecture key | mistral3; also llama for Mistral Small 3.x files converted before llama.cpp had mistral3 (see llama-labelled files) |
| Source class | Mistral3Model (single-sequence) + Mistral3Model.BatchedForward.cs (IBatchedPagedModel) |
| Vision encoder | Mistral3VisionEncoder (Pixtral) |
| Image processor | Mistral3ImageProcessor |
| Example models | Mistral-Small-3.1-24B-Instruct, Ministral-3-14B-Instruct |
| Modalities | Text, image |
| Thinking mode | No |
| Tool calling | No — the renderer declares no tools and drops role: "tool" messages, so the code tools and sub-agent delegation are not offered and skills fall back to inlined instructions |
| Speculative decoding | No — Mistral 3 has no speculative trunk, so --spec (the weight-free n-gram drafter included) serves standard decode |
| Batched / paged forward | Default — reference IBatchedPagedModel.ForwardBatch. Verified end-to-end on Ministral-3-14B; native paged-attention kernel ~21% faster than legacy on long context. See §11. |
| Output parser | PassthroughOutputParser |
Verified GGUF pointer (the Pixtral mmproj lives in the same repo):
| Model | HF repo | Recommended file | Pixtral mmproj |
|---|---|---|---|
| Mistral-Small-3.1-24B-Instruct-2503 | bartowski/mistralai_Mistral-Small-3.1-24B-Instruct-2503-GGUF | mistralai_Mistral-Small-3.1-24B-Instruct-2503-Q4_K_M.gguf (14.334 GB) or mistralai_Mistral-Small-3.1-24B-Instruct-2503-Q8_0.gguf (25.055 GB) |
mmproj-mistralai_Mistral-Small-3.1-24B-Instruct-2503-f16.gguf (0.878 GB; same repo) |
The bartowski files above declare general.architecture = llama and keep every
hyperparameter under llama.*: they were converted before llama.cpp had a
mistral3 architecture. Until this was handled they failed at load with
Unsupported architecture: llama.
llama is not an alias of mistral3, because the same label also names
Llama 2/3, Mixtral and others that the Mistral graph would run into fluent
garbage. Instead the registry offers a llama-labelled file to
Mistral3Architecture.IsLlamaLabelledMistral3
(ModelArchitectureDescriptor.RecognizeRelabelledFile), which accepts it only
when every one of these holds:
| Check | Refuses |
|---|---|
tokenizer.ggml.pre = tekken |
SentencePiece Mistral 7B / Mixtral, Llama 3 BPE |
the vocabulary has [INST], [/INST], [SYSTEM_PROMPT], [/SYSTEM_PROMPT] (the renderer emits them) |
Tekken vocabularies without those control tokens |
no llama.expert_count, no blk.0.ffn_gate_inp.weight |
MoE files |
no rope_freqs.weight, and llama.rope.scaling.type absent, none or yarn |
Llama 3.1-style frequency scaling this model does not apply |
no attn_q_norm / attn_k_norm; the dense attention and SwiGLU tensors are present |
families with other block layouts |
An accepted file loads as Mistral3Model. Config.Architecture stays mistral3,
so the chat renderer, output parser and capability tables all select Mistral 3,
while the hyperparameters and context length are read under the llama.* prefix
the file actually uses (ModelBase.MetadataArchitecture). Startup prints
GGUF labelled 'llama' is served as mistral3. A llama file that fails a check is
still refused, and the message now names what mistral3 would have accepted.
The companion projector name mmproj-mistralai_Mistral-Small-3.1-...-f16.gguf
matches the auto-discovery hint *mmproj*istral*.gguf, so the CLI finds it
beside the model without --mmproj. The server still takes it through --mmproj.
bartowski's mmproj-...-f16.gguf is a llama.cpp clip file (clip.projector_type = pixtral). The encoder was written against Ollama's projector, and on this file
every image request first failed with HTTP 500
(KeyNotFoundException: 'v.patch_conv.weight'). Once that was fixed the model was
still blind: it read a red card showing 4821 as a blue 2975. Comparing the
encoder, on the real projector and image, with a transcription of Hugging Face's
Pixtral tower and Mistral3PatchMerger located every difference:
| Difference | Fix |
|---|---|
Tensor names: v.patch_embd, v.pre_ln, v.blk.N.ln1/ln2/attn_out, mm.input_norm, mm.patch_merger, mm.1, mm.2 |
Renamed at load (Mistral3VisionEncoder.CanonicalTensorName); the Ollama names still load unchanged |
llama.cpp's converter permutes the vision Q/K rows into interleaved RoPE pairs (LlamaModel.permute) |
A llama.cpp file's Q/K rows are put back into Hugging Face's rotate-half layout (UnpermuteInterleavedRows) |
The vision MLP was hard-coded to SiLU. Mistral Small 3.1's tower is GELU-gated (hidden_act = "gelu", stored as clip.use_gelu) |
The activation comes from clip.use_gelu / clip.use_silu. A file with neither gets GELU, which is also PixtralVisionConfig's default |
| The 2D RoPE table was written frequency-major but read patch-major, so every patch got another patch's angles | Built patch-major (BuildVisionRopeAngles) |
The patch merger gathered each 2x2 window patch by patch. torch.nn.functional.unfold is channel-major |
Channel-major (MergePatches) |
The Mistral 3 renderer (it always renders its own template) skipped the media-placeholder pass. The prompt had no [IMG], so the encoded image was dropped |
RenderMistral3 emits one [IMG] per image before the text |
The injector copied the rows x cols patch embeddings contiguously over a span whose tokens put an [IMG_BREAK] after every row. Each later row slid one more position onto the markers |
The embedding is laid out like the tokens: each row of patches is followed by the [IMG_BREAK] token embedding, or [IMG_END] after the last row (LayOutMistral3ImageRows), as llama.cpp does |
After the fixes, the 391 merged embeddings for a 640x480 image match the reference with a minimum per-token cosine of 0.99999 (managed CPU path). Put back SiLU, the old merge order or the permuted Q/K, one at a time, and the mean per-token cosine falls to 0.69, 0.08 or 0.37. A projector that is missing a tensor the encoder needs, or that has linear biases the encoder does not apply, is now refused at load with a message naming both accepted layouts. Before, the encoder skipped a missing norm silently and crashed on a missing linear at the first image.
On the release media fixtures
(validate_deepseek41_media.py --scenarios image_ocr,multi_image,image_follow_up --concurrency 1,4, Q4_K_M + f16 mmproj, ggml_cuda) TensorSharp passes
image_ocr 5/5 and image_follow_up 5/5 and fails multi_image 0/5. On the
two-image prompt it reads both codes with the last digit missing (482, 936).
llama.cpp's llama-server on the same files scores image_ocr 2/5,
image_follow_up 3/5 and multi_image 2/5. Its failures give the same
three-digit answer (["482", "936"]), and it misreads 4821 alone as 0482.
The two-image miss is this checkpoint's reading, not an injection error:
TensorSharp's log places the second image at the position right after the first
image's [IMG_END].
Only the llama.cpp projector was checked against the reference. No Ollama-layout
projector was available, and one loads under the same rules: no Q/K un-permute, and
GELU unless it declares clip.use_silu.
The conversion identifies mistralai/Mistral-Small-3.1-24B-Instruct-2503 as its official upstream; both model cards declare Apache-2.0.
Command-line download (one line per file; requires pip install -U huggingface_hub):
python -m pip install -U huggingface_hub
hf download bartowski/mistralai_Mistral-Small-3.1-24B-Instruct-2503-GGUF mistralai_Mistral-Small-3.1-24B-Instruct-2503-Q4_K_M.gguf --local-dir models
hf download bartowski/mistralai_Mistral-Small-3.1-24B-Instruct-2503-GGUF mmproj-mistralai_Mistral-Small-3.1-24B-Instruct-2503-f16.gguf --local-dir modelsCLI one-shot with an image (Pixtral vision needs the projector; the CLI finds
a *mmproj*istral*.gguf beside the model on its own, otherwise pass --mmproj;
with --image and no --input a default describe-the-image prompt is used; CLI sampling
defaults to greedy and --max-tokens defaults to 100):
dotnet run --project TensorSharp.Cli -c Release -- --model models/mistralai_Mistral-Small-3.1-24B-Instruct-2503-Q4_K_M.gguf \
--mmproj models/mmproj-mistralai_Mistral-Small-3.1-24B-Instruct-2503-f16.gguf \
--image photo.png --max-tokens 512 --backend ggml_cudaServer (Web UI + OpenAI/Ollama-compatible APIs on http://localhost:5000):
dotnet run --project TensorSharp.Server.Host -c Release -- --model models/mistralai_Mistral-Small-3.1-24B-Instruct-2503-Q4_K_M.gguf \
--mmproj models/mmproj-mistralai_Mistral-Small-3.1-24B-Instruct-2503-f16.gguf \
--backend ggml_cuda --max-tokens 4096Mistral 3 is Mistral's third-generation LLaMA-style dense transformer with two distinguishing extensions:
- YaRN-corrected RoPE — partial, per-frequency interpolation between the original RoPE schedule and an extrapolated one. This lets the model attend cleanly across context lengths well past the original training context.
- Position-dependent Q scaling — once the position exceeds the original
context length, Q is multiplied by
1 + β · log(1 + ⌊pos / origCtx⌋), which keeps attention magnitudes well-conditioned at long ranges. - Pixtral vision encoder for image input.
Otherwise the architecture is the standard LLaMA / Mistral recipe: GQA, no QK-norm, SwiGLU FFN, RMSNorm, GPT-J style RoPE pairings, optional tied LM head.
tokens (int[])
│
token_embd.weight
│
[optional] InjectVisionEmbeddings (image tokens)
│
┌──── × NumLayers ──────────────────────────────┐
│ RMSNorm(attn_norm) │
│ Q, K, V projections (or fused QKV) │
│ RoPE_GPT-J(Q, K) with YaRN-corrected freqs │
│ Q ← Q * (1 + β · log(1 + ⌊pos / origCtx⌋)) │
│ attention(Q, KCache, VCache) (full causal) │
│ attn_output ─► residual │
│ RMSNorm(ffn_norm) │
│ ffn_gate_up ─► [gate ‖ up] │
│ SwiGLU: SiLU(gate) * up │
│ ffn_down ─► residual │
└────────────────────────────────────────────────┘
│
RMSNorm(output_norm)
│
LM head (output.weight or tied)
│
▼
logits
For each layer L:
hidden ─► RMSNorm(attn_norm.weight, eps)
─► QKV (fused via attn_qkv.weight when fused at load time, otherwise
three separate attn_q/k/v matmuls)
─► RoPE_GPT-J(Q, K, ropeFreqs[]) at positions startPos..startPos+seqLen-1
• GPT-J style: pairs adjacent elements (x[2i], x[2i+1])
• Frequencies are YaRN-corrected when ropeType == "yarn"
─► [if _ropeOrigCtx > 0]:
scale = 1 + _ropeScalingBeta * log(1 + floor(pos / _ropeOrigCtx))
Q ← Q * scale
─► Q ← Q * (1/sqrt(headDim))
─► append (K, V) to KV cache
─► attention(Q, KCache, VCache) # standard causal GQA
─► attn_output.weight matmul → o
─► hidden = hidden + o
─► h2 = RMSNorm(ffn_norm.weight, eps) on hidden
─► ffn_gate_up.weight matmul → [gate ‖ up]
─► g = SiLU(gate)
─► h3 = ffn_down.weight × (g * up)
─► hidden = hidden + h3
After all layers:
hidden ─► narrow(seq_len-1) if prefill
─► RMSNorm(output_norm.weight, eps)
─► matmul against output.weight (or token_embd.weight when tied)
─► copy to float[VocabSize]
- GQA with
Config.NumKVHeads < Config.NumHeads. Separatekey_lengthandvalue_lengthare supported (_attnKeyLen,_attnValLen); they default toConfig.HeadDimwhen not set. - Optional fused QKV.
FuseQKVWeights()buildsattn_qkv.weightwhen the GGUF stores Q / K / V separately. The decoder uses_layerQkvFused[l]to pick the fused matmul vs. the three separate matmuls per layer. - No QK-norm — Mistral 3 skips the per-head RMSNorm of Q and K.
-
Pairing: adjacent elements
(x[2i], x[2i+1])are rotated together (GPT-J convention), as opposed to the(x[i], x[i + halfDim])pairing of NeoX. -
YaRN frequency correction: when
mistral3.rope.scaling.type == "yarn"andmistral3.rope.scaling.original_context_length > 0,ApplyYarnFreqCorrection()interpolates between extrapolated and interpolated frequencies based on whether each frequency band is in the "slow" or "fast" rotation range:lowFreqWavelen = origCtx / betaSlow highFreqWavelen = origCtx / betaFast for each freq f: wavelen = 2π / f if wavelen < highFreqWavelen: # high-freq band: extrapolate f stays elif wavelen > lowFreqWavelen: # low-freq band: interpolate f *= 1 / scale else: # medium band: smooth interp ramp = ... # linear ramp between the two f = mix(f, f / scale, ramp) -
Position-dependent Q scaling: when
_ropeOrigCtx > 0,q *= 1 + _ropeScalingBeta * log(1 + floor(pos / _ropeOrigCtx))This keeps attention magnitudes well-conditioned past the original context length.
ffn_gate_up.weight packs [gate ‖ up]. Ops.SiLUMul then computes
SiLU(gate) * up, followed by ffn_down. No biases.
token_embd.weight is the input embedding. The LM head uses
output.weight when present; otherwise it's tied to token_embd.weight
(_hasTiedOutput).
Mistral3VisionEncoder is the Pixtral architecture:
- Conv2D patch embedding:
v.patch_conv.weight(shape[hidden, channels, patchSize, patchSize]) with optional bias. - RMSNorm at the encoder input.
- 2D RoPE positional embedding for spatial position.
- Gated MLP transformer blocks: RMSNorm + multi-head attention +
residual + RMSNorm + gated MLP + residual. The gate activation is GELU for
Mistral Small 3.1 (
clip.use_gelu);clip.use_siluselects SiLU. - Spatial patch merging: groups neighboring patches into one merged token at the projector boundary.
- Multimodal projector: RMSNorm → PatchMerger → Linear → GELU → Linear, mapping to the LM hidden dim.
Mistral3ImageProcessor resizes images preserving aspect ratio:
- Composite RGBA over a white background.
- Resize so the longest edge equals
longest_edgewhile keeping aspect ratio. - Pad to a multiple of
patch_size. - Normalize with the CLIP default mean / std.
The multimodal injector (ProcessMistral3History in
ModelMultimodalInjector.cs) walks the chat history, runs the encoder once
per unique image, and queues a PreparedEmbeddingSpan so the embeddings
land at the right positions in the prompt before Forward().
| Key | Type | Meaning |
|---|---|---|
mistral3.rope.dimension_count |
uint32 | RoPE dimension count |
mistral3.rope.scaling.type |
string | RoPE scaling type (e.g. yarn) |
mistral3.attention.temperature_scale |
float32 | Position-dependent Q scaling β (default 0.1) |
mistral3.rope.scaling.original_context_length |
uint32 | YaRN original context length |
mistral3.rope.scaling.extrapolation_factor |
float32 | YaRN extrapolation factor (default 1.0) |
mistral3.rope.scaling.yarn_beta_fast |
float32 | YaRN fast rotation threshold (default 32.0) |
mistral3.rope.scaling.yarn_beta_slow |
float32 | YaRN slow rotation threshold (default 1.0) |
mistral3.rope.scaling.mscale |
float32 | YaRN mscale (default 0) |
mistral3.rope.scaling.mscale_all_dim |
float32 | YaRN mscale_all_dim (default 0) |
For the Pixtral vision projector (mmproj GGUF), the standard
clip.vision.* keys cover the encoder dims, plus mm.* keys for the
projector layers.
token_embd.weight # [vocab, hidden]
blk.{L}.attn_norm.weight # pre-attention RMSNorm
blk.{L}.attn_q.weight # Q projection (before fusion)
blk.{L}.attn_k.weight # K projection (before fusion)
blk.{L}.attn_v.weight # V projection (before fusion)
blk.{L}.attn_qkv.weight # fused Q+K+V (after fusion)
blk.{L}.attn_output.weight # output projection
blk.{L}.ffn_norm.weight # pre-FFN RMSNorm
blk.{L}.ffn_gate.weight } # before fusion
blk.{L}.ffn_up.weight }
blk.{L}.ffn_gate_up.weight # after fusion: [2*intermediate, hidden]
blk.{L}.ffn_down.weight # FFN down projection
output_norm.weight # final RMSNorm
output.weight # LM head (optional if tied)
v.patch_conv.weight # Conv2D patch embedding [hidden, C, P, P]
v.patch_conv.bias # Conv2D bias (optional)
v.encoder_norm.weight # encoder input RMSNorm
v.blk.{L}.attn_norm.weight # pre-attention RMSNorm
v.blk.{L}.attn_q.weight # Q projection
v.blk.{L}.attn_k.weight # K projection
v.blk.{L}.attn_v.weight # V projection
v.blk.{L}.attn_output.weight # output projection
v.blk.{L}.ffn_norm.weight # pre-FFN RMSNorm
v.blk.{L}.ffn_gate.weight # gate (GELU, or SiLU with clip.use_silu)
v.blk.{L}.ffn_up.weight # up projection
v.blk.{L}.ffn_down.weight # down projection
mm.norm.weight # projector RMSNorm
mm.patch_merger.merging_layer.weight # spatial patch merger
mm.linear_1.weight # projector linear 1
mm.linear_2.weight # projector linear 2
These are Ollama's names. A llama.cpp projector names the same tensors
v.patch_embd, v.pre_ln, v.blk.{L}.ln1 / ln2 / attn_out, mm.input_norm,
mm.patch_merger and mm.1 / mm.2, and stores the Q/K rows permuted; see
llama.cpp projector files.
Constructor (Mistral3Model(string ggufPath, BackendType backend)):
ParseBaseConfig().- Reads
_attnKeyLen/_attnValLenfromKeyLength/ValueLength(or defaults toHeadDim). Reads_ropeDim. - Reads YaRN parameters.
ParseTokenizer(),LoadWeights().FuseQKVWeights(),FuseGateUpWeights().PrepareCudaQuantizedWeightsForInference().InitKVCache(maxSeqLen)allocates per-layer K and V tensors of shape[NumKVHeads, maxSeqLen, headDim](separate_attnKeyLen/_attnValLenare honored).PrecomputeConstants()builds:- The per-layer weight name arrays — different shapes depending on whether QKV / gate-up are fused for that layer.
- The RoPE frequency table
_ropeFreqs[halfDim]. - YaRN-corrected frequencies via
ApplyYarnFreqCorrection()when_ropeType == "yarn"and_ropeOrigCtx > 0.
Forward(int[] tokens) runs the per-op managed loop:
- Embedding lookup.
- Optional vision injection at the positions of the expanded
[IMG]rows. - For each layer: RMSNorm, QKV (fused or split), RoPE with YaRN-corrected frequencies, optional position-dependent Q scaling, attention, output projection + residual, FFN, residual.
- For prefill, the residual is narrowed to the last token before
output_normso the LM head matmul produces a single row. - Final RMSNorm, LM head, copy to
_logitsBuffer.
- Fused QKV and gate / up cut the per-layer GEMM count.
- Last-row narrow before
output_normso the LM head matmul only produces[1, vocab]. - YaRN frequency table built once at load time. The decode path uses
the same
_ropeFreqs[]array. - Backend ops dispatch through
Ops.RoPEExandOps.SiLUMul, which run as native kernels on GGML / CUDA backends.
There is no chunked prefill (full attention only — no SWA), no fused per-layer prefill kernel, and no native whole-model decode for Mistral 3. Adding either would shrink decode latency and TTFT.
- Pre-resolved per-layer weight name arrays (
_layerWeightNames[L][]) allocated inPrecomputeConstants()keep the hot loop allocation-free. - The
_layerQkvFused[]flag picks the fused vs. split QKV path per layer without allocations. - Pre-allocated decode position arrays and YaRN-corrected
_ropeFreqs[]avoid recomputation per token. - Quantized weights stay quantized in
_quantWeights; matmul calls run through the backend's native quantized matmul.
- Per-layer K and V tensors of shape
[NumKVHeads, maxSeqLen, headDim]using_attnKeyLen/_attnValLenfor the per-head dim. ResetKVCache()zeroes everything and callsInvalidateTensorDeviceCache()to sync GPU state.TruncateKVCache(int tokenCount)is supported (used for multi-turn KV reuse on the server).- The Pixtral vision encoder dequantizes its weights to F32 at load time; the LM weights stay quantized when supported by the backend.
Mistral 3 is the reference implementation of IBatchedPagedModel.ForwardBatch
in TensorSharp (Mistral3Model.BatchedForward.cs).
It runs through the shared continuous-batching engine (InferenceEngine +
ContinuousBatchScheduler + BatchExecutor) described in
docs/PAGED_ATTENTION_AND_CONTINUOUS_BATCHING.md.
Key properties:
- Default-on, no opt-in env var. Continuous batching for Mistral 3 is
always available;
--no-continuous-batching(server and CLI) forces the per-sequence KV-swap path for every model, including Mistral 3. - Per-layer paged K/V buffers of layout
[numBlocks * blockSize * numKvHeads * headDim], lazily grown byEnsurePagedBuffersAllocated. The "grow" path copies existing K/V into the new buffer so already-scheduled sequences don't lose their state. - Fused vs. unfused QKV per layer —
ForwardBatchbranches on_layerQkvFused[layer]to pick between the fusedattn_qkv.weightmatmul and the three independentattn_q/k/v.weightmatmuls. Both paths batch across the entire token axis in a single GEMM per layer. - Per-token YaRN RoPE. The
_ropeFreqs[]array built at load time is applied throughOps.RoPEExwith an explicitpositions[]array, and per-token YaRN position-dependent Q scaling (q *= 1 + beta * log(1 + floor(pos / orig_ctx))) is performed inside the batched loop so each token in the batch picks its own scale factor. - K/V scatter via
slotMappingwrites fresh K and V into the layer's paged buffer atblockId * blockSize + offset. No KV-state extract / inject between sequences in the batched path. - Paged attention by backend:
- GGML backends:
TSGgml_PagedAttentionForward— C++ memcpy gather of K/V from the paged buffer per sequence, thenggml_flash_attn_ext(the same fused Metal/CUDA flash attention kernel the per-sequence path uses). - Other backends:
ManagedPagedAttention.Forward— pure-C# online-softmax loop, parallelised over(seq, head).
- GGML backends:
- Vision-embedding injection runs upstream of the per-layer loop (same path as the per-sequence forward). The multimodal injector serialises prompt preparation behind a lock for multimodal turns; text-only turns prepare in parallel.
Verified correctness on the real GGUF
(Mistral3BatchedForwardTests):
Mistral3_BatchSize1_ForwardBatchMatchesLegacyTop1— legacy top-1 = batched top-1 = 1091 on a 6-token probe prompt.Mistral3_BatchSize2_DistinctSequencesProduceDistinctLogits— two diverging prompts in one batch produce distinct top-1 tokens, proving the K/V is correctly partitioned.
Measured throughput on Ministral-3-14B-Instruct-2512-Q4_K_M (Apple M4 Pro, GgmlMetal, 4 parallel chat-style prompts):
| Path | Long context (~800 tok/seq) wall | Tokens/sec | vs. legacy |
|---|---|---|---|
| Batched + Native kernel (default) | 42.37 s | 0.6 | 1.21× |
| Per-sequence KV-swap (legacy GGML) | 53.95 s | 0.4 | 1.0× |
| Batched + Tensor kernel | 71.42 s | 0.3 | 0.76× |
| Batched + Managed kernel | 111.02 s | 0.2 | 0.49× |
The native kernel beats the legacy GGML per-sequence path by ~21% on long context — the headline result the whole continuous-batching effort has been building toward.
Prefix-cache validation: in the same long-context run the engine
shared six full prompt blocks across the four sequences
(reused=1536, hashedCached=3), exercising the block-hash prefix
cache end-to-end on a real GGUF. That run predates the Radix prefix cache,
which replaced the block-hash sharing it measured (Mistral 3 takes part as a
page family).
PassthroughOutputParser— Mistral 3 has no thinking / tool-call wire format.- The chat template is always TensorSharp's own
ChatTemplate.RenderMistral3(the protocol prefers it over the GGUF's Jinja): a leading system message becomes[SYSTEM_PROMPT]...[/SYSTEM_PROMPT], each user turn[INST]...[/INST], and assistant turns are appended as plain text. Tool declarations androle: "tool"messages are not rendered. - Each image is one
[IMG]placeholder emitted before the user text. The injector (ProcessMistral3History) expands it into the image's rows of[IMG]tokens, each row followed by[IMG_BREAK]and the last by[IMG_END](see llama.cpp projector files).
- Native whole-model decode — the entire legacy forward pass is managed C# with backend-dispatched matmul. A native single-call decode path would remove most of the managed overhead from the per-seq path.
- Fused single-graph decode — a
Mistral3ModelDecodeGGML kernel would significantly improve Metal / CUDA throughput by collapsing the per-layer dispatches in the legacy path. - Quantized vision encoder — Pixtral weights are currently dequantized to F32 at load time, increasing memory usage for image-heavy workloads. Supporting quantized vision weights directly would shrink the resident set and cut load time.
- Fused prefill attention — adopting Qwen 3.5's
FusedPrefillAttentionapproach would reduce per-layer prefill round-trips on GGML / CUDA. - One batched ggml graph for the whole batch —
ForwardBatchcurrently builds a separate paged-attention graph per sequence per layer. Folding them into a single per-layer graph would amortise the compile / launch overhead for batches with many short sequences.