Skip to content

ARCH: quant-type dispatch is duplicated across ~30 files with no single source of truth — propose a dedicated consolidation release #3418

Description

@alfredodeza

tl;dr

Aprender has no equivalent of llama.cpp's ggml_type_traits[] — the single
table that tells the entire codebase what a quant type is (block size,
byte size, dequant fn) once, so every operation and every model architecture
gets it for free. Instead, ~30 files in crates/aprender-serve/src/ each
independently match on quant type. The result is that "does architecture X
work" is not actually one question — it's architectures × quant_types × backends × call_sites, and today most of that matrix is untested. Five
closed tickets (#1749, #1789, #2535, #3341, #3091) are the same defect class
recurring in different cells of that matrix, each discovered by a user
crash rather than by a gate that could have covered all of them at once.

This is not a "fix Qwen" ticket — #3090/#3091/#3413 stay where they are for
the specific model-family defects. This is the structural ticket: the
dispatch architecture itself is the reason those keep happening
, and it
will keep happening for every future model (Qwen4 preview is already sitting
in a Downloads folder untested) unless the dispatch is consolidated once.

Evidence

1. The size gap between "new architecture" costs, aprender vs llama.cpp

llama.cpp's entire Qwen3-MoE architecture — tensor shapes, attention, RoPE,
MoE routing — is 179 lines (src/models/qwen3moe.cpp in a local llama.cpp
checkout, current as of 3173a5647 2026-08-29). It's short because it's
almost entirely calls into shared, quant-agnostic building blocks
(build_qkv, build_attn, build_moe_ffn, ggml_rope_ext) that every one
of llama.cpp's ~50 architectures reuses unchanged.

Aprender's Qwen3-MoE-specific code alone
(crates/aprender-serve/src/gguf/qwen3_moe_load.rs +
crates/aprender-serve/src/infer/qwen3_moe_generate.rs) is 1,678 lines —
before counting anything it depends on.

2. Quant-type dispatch is scattered, not centralized

$ grep -rln "GGUF_TYPE_Q4_K\|GGUF_TYPE_Q4_0\|match qtype\|match.*\.qtype" \
    crates/aprender-serve/src/ --include="*.rs" | grep -v test | wc -l
30

30 files, each with its own quant-type match arms:
gguf/transformer.rs, gguf/inference/fused_matmul_into.rs,
gguf/inference/matmul_fused.rs, gguf/qwen3_moe_load.rs,
gguf/metadata.rs, cuda/executor/layers/gemv_dispatch.rs,
gpu/adapters/wgpu_adapter.rs, and 23 more (full list in the investigation
that produced this ticket — available on request / in session transcript).

llama.cpp's equivalent is ggml_type_traits[]
(ggml/src/ggml.c:~750-900) — one array, one entry per type
(type_size, blck_size, to_float, from_float), read generically by
every operation in the compute graph.

Aprender has a partial version of this idea —
QuantBlockFormat (crates/aprender-serve/src/quantize/format_trait.rs) —
but only 5 of the ~30 call sites are known to route through it consistently,
and the trait itself only models affine quantization (x = d*s*q - dmin*m: Q4_0/Q8_0/Q4_K/Q5_K/Q6_K). It cannot represent the IQ family
(IQ2_XXS/XS, IQ3_XXS/S, IQ4_NL/XS), which are codebook/lattice
quantization — each block indexes into a fixed grid table, not a per-block
affine scale. That's a second trait shape needed, not an extension of the
first.

3. The universe aprender tracks is smaller than the universe it needs to track

$ grep -rohE "GGUF_TYPE_[A-Z0-9_]+" crates/aprender-serve/src/ | sort -u
GGUF_TYPE_ARRAY  GGUF_TYPE_BF16  GGUF_TYPE_F16  GGUF_TYPE_F32
GGUF_TYPE_INT32  GGUF_TYPE_Q2_K  GGUF_TYPE_Q3_K  GGUF_TYPE_Q4_0
GGUF_TYPE_Q4_1   GGUF_TYPE_Q4_K  GGUF_TYPE_Q5_0  GGUF_TYPE_Q5_1
GGUF_TYPE_Q5_K   GGUF_TYPE_Q6_K  GGUF_TYPE_Q8_0  GGUF_TYPE_STRING
GGUF_TYPE_UINT32

16 constants named anywhere in the crate. The GGUF spec defines 30+
(missing here: the entire IQ1/IQ2/IQ3/IQ4 family, Q8_1, Q8_K, TQ1_0/TQ2_0).
Every one of those missing types is a RealizarError::UnsupportedOperation
waiting to be hit by the next real-world download, in whichever of the 30
files happens to load the tensor first.

4. The ticket pattern — same defect, different cells of the matrix

Issue Architecture Broke because
#1749 MoE (generic) matmul_fused.rs:211 index OOB on MoE GGUF
#1789 Qwen3-Coder-30B MoE Same line, F32 weight this time
#2535 MoE (generic) Misdiagnosed as "truncated/corrupt" — the real cause was an unhandled tensor shape/qtype combination
#3341 Qwen3-Coder-30B MoE Q4_0 experts refused — the MoE loader's qtype list only had Q4_K/Q6_K
#3091 Qwen3.5 (qwen35) IQ2_XS/IQ4_XS refused in tensor_byte_size — that function's qtype list only had 9 of the ~30 real types

Five tickets, five separate fixes, one root cause each time: "this
particular file's quant-type list was incomplete."
None of these fixes
closed the class — each is scoped to the one file that crashed. The next
new model (Qwen4-preview architecture is already sitting untested in
~/Downloads/Qwen3.8-Flash-Next-UD-Q2_K_XL-...gguf, per #3413's sibling
investigation) will find the next incomplete list, in some 31st file.

Proposed consolidation

  1. A QuantCodebookFormat trait alongside the existing
    QuantBlockFormat, covering lattice/codebook quant types. Grid tables
    are portable data — llama.cpp's ggml-quants.c iq2xs_grid/iq3xs_grid
    etc. are the reference, not code to be creatively reinvented.
  2. A single dispatch entry point analogous to ggml_type_traits[]:
    given a qtype: u32, return {byte_size, dequant_to_f32, family} in one
    place. Every one of the ~30 call sites is refactored to call through it
    instead of maintaining its own arm list.
  3. A completeness gate: a test (or pv contract) that enumerates every
    GGUF_TYPE_* the format spec defines and asserts the dispatch table has
    an entry for each — so a missing type is a CI failure at merge time, not
    a user's crash report three months later. This is the mechanical
    backstop; CPU: Qwen3.5 / Qwen3.8 hybrid GGUFs are refused entirely — no fallback exists #3091's own history shows the honor-system approach (each file
    remembering to update its own list) has failed five times already.
  4. Per-backend fan-out is then additive, not multiplicative: CPU
    SIMD/CUDA/wgpu each implement the trait once per type; the ~30
    call-site refactor happens once regardless of how many backends exist.

Why this should be its own release, not squeezed into the current cadence

Per docs/specifications/06x-release-schedule.md, this project runs a
2-3 day release cadence, 24/7 (v0.66.0 → v0.70.0 in the current
train). That cadence is built for landing scoped, independently-shippable
fixes — exactly the shape of #3091, #3341, #3413. It is the wrong cadence
for this ticket, for three reasons:

  • Blast radius. The refactor touches ~30 files across the CPU, CUDA,
    and wgpu backends simultaneously — nearly every file in
    crates/aprender-serve/src/gguf/, quantize/, cuda/, and gpu/.
    Landing it incrementally alongside unrelated model-fix PRs on a 2-3 day
    cadence means every one of those PRs is rebasing against a moving
    dispatch layer, and every dequant/matmul kernel needs re-verification
    against the same parity bar this project already holds itself to (per
    CPU: Qwen3.5 / Qwen3.8 hybrid GGUFs are refused entirely — no fallback exists #3091's own history: cosine ≥0.98 vs llama.cpp, per quant type, per
    backend). That verification matrix (≥9 missing quant types × 3 backends)
    is itself real work, not a rename.
  • Regression risk to the one thing that currently works. Qwen2.5-Coder
    (README's demonstrated, BEATS.md-measured, GPU-parity model) runs through
    several of these same 30 files today. A consolidation that isn't done in
    one dedicated, fully-gated pass risks the exact kind of silent regression
    this ticket exists to prevent — on the one model line that currently has
    real proof behind it.
  • It competes for the same attention the model-specific tickets need.
    GPU: implement Gated DeltaNet / SSM inference for Qwen3.5 / Qwen3.8 hybrid GGUFs #3090 (GPU), P0: dense Qwen3 (Qwen3ForCausalLM) has no current proof it runs — last verification is 7 months stale and CI never covers it #3413 (dense Qwen3 proof), and whatever Qwen4-preview turns
    into all sit downstream of this dispatch layer. Doing them first means
    redoing parts of them once this lands; doing this first means every
    future model ticket gets cheaper, not just the current ones.

Ask: reserve one full release cycle (e.g. a dedicated milestone, not
squeezed as a rider into 0.69/0.70/0.71) where this consolidation is the
only architectural change landing
, with model-specific bug fixes limited
to true P0 hotfixes during that window. The exit criterion is the
completeness gate in (3) above going green, plus a full parity re-run of
the existing Qwen2.5-Coder GPU-beat measurement to prove no regression.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions