You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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
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
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
A QuantCodebookFormat trait alongside the existing QuantBlockFormat, covering lattice/codebook quant types. Grid tables
are portable data — llama.cpp's ggml-quants.ciq2xs_grid/iq3xs_grid
etc. are the reference, not code to be creatively reinvented.
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.
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.
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.
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.
tl;dr
Aprender has no equivalent of llama.cpp's
ggml_type_traits[]— the singletable 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/eachindependently
matchon quant type. The result is that "does architecture Xwork" is not actually one question — it's
architectures × quant_types × backends × call_sites, and today most of that matrix is untested. Fiveclosed 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.cppin a local llama.cppcheckout, current as of
3173a56472026-08-29). It's short because it'salmost entirely calls into shared, quant-agnostic building blocks
(
build_qkv,build_attn,build_moe_ffn,ggml_rope_ext) that every oneof 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
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 investigationthat 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 byevery 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
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::UnsupportedOperationwaiting 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
matmul_fused.rs:211index OOB on MoE GGUFqwen35)tensor_byte_size— that function's qtype list only had 9 of the ~30 real typesFive 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 siblinginvestigation) will find the next incomplete list, in some 31st file.
Proposed consolidation
QuantCodebookFormattrait alongside the existingQuantBlockFormat, covering lattice/codebook quant types. Grid tablesare portable data — llama.cpp's
ggml-quants.ciq2xs_grid/iq3xs_gridetc. are the reference, not code to be creatively reinvented.
ggml_type_traits[]:given a
qtype: u32, return{byte_size, dequant_to_f32, family}in oneplace. Every one of the ~30 call sites is refactored to call through it
instead of maintaining its own arm list.
pvcontract) that enumerates everyGGUF_TYPE_*the format spec defines and asserts the dispatch table hasan 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.
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 a2-3 day release cadence, 24/7 (
v0.66.0→v0.70.0in the currenttrain). 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:
and wgpu backends simultaneously — nearly every file in
crates/aprender-serve/src/gguf/,quantize/,cuda/, andgpu/.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.
(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.
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
investigation)
layer)
apr benchpanics on MoE GGUF models (matmul_fused.rs:211 index OOB) #1749, apr serve: matmul_fused.rs:211 panics with 'index out of bounds: len 0' on Qwen3-Coder-30B-MoE F32 weight #1789, MoE GGUF misdiagnosed as 'truncated/corrupt … file is incomplete' — the file is provably complete, and apr validate agrees #2535, CPU+GPU: Qwen3-Coder-30B MoE (Q4_0) fails with empty-buffer error on blk.0.ffn_gate.weight — possible #1789 regression #3341 — prior instances of the same defect classaspirational, once (3)'s completeness gate exists)