The semantic IR: QuantScheme
QuantScheme (layers/quant/scheme.py) is pure semantics. It carries no scale layout, kernel, or device decision. A layer’s quantization decision is exactly one QuantScheme:
TensorQuant, so input=None cleanly means weight-only. The fields of TensorQuant:
This layer decides nothing on purpose, so that config parsing can land first. An importer only has to build a
QuantScheme; it does not care whether the target hardware has a matching kernel.
From semantics to physical: materialize
materialize(scheme, sm) (layers/quant/materialize.py) lowers semantics into a physical WeightSpec, and it is the only place that reads hardware information:
sm >= 100 (Blackwell) uses the 128x4 layout that FlashInfer wants, otherwise the reference linear layout. Types defined in QDType but not wired here (int8 / int4 / mxfp4 / fp8_e5m2) raise NotImplementedError.
The physical layer: WeightSpec
WeightSpec (layers/quant/base.py) is a Protocol, independent of the op (linear, embedding, MoE). It only sees an AllocationRequest, a “where and how big” packet the layer fills from its own shape conventions and hands over:
spec registers weight and any scale parameters on the layer, but does not write op-specific fields like input_size_per_partition; that is the layer’s job. The three concrete specs:
Bf16Spec
The simplest one: it allocates a singlenn.Parameter and no scale. It carries neither granularity nor needs_act_quant, because bf16 is not a quant format. weight_dtype is only a hint; the real dtype comes from request.params_dtype, so fp16 needs no new spec.
Fp8Spec
The weight istorch.float8_e4m3fn, with one of three scale granularities:
PER_TENSOR: one scalar per logical matrix plus one static activation scale. After loading,process_after_loadingfans it out to per-channel so kernels see a uniform scale layout.PER_CHANNEL: one weight scale per output row; the activation is quantized per token (rowwise) at runtime.BLOCK: the weight scale is(out // block_n, in // block_k); the activation is quantized per token at block-K granularity.
LinearActivationQuant (layers/quant/linear.py), quantizing the activation into an ActivationView through quantize_activation before the matmul. With needs_act_quant=True, a kernel uses isinstance(spec, LinearActivationQuant) to decide whether to take this path.
Nvfp4Spec
NVFP4 packs two E2M1 values per byte, so the logical(N, K) weight is stored as (N, K // 2); every 16 values along K share one FP8-E4M3 block scale, plus one fp32 global scale. Two scale layouts:
It does one thing the other two specs do not: quantize from a high-precision checkpoint on the fly. When
load_weight sees an incoming bf16/fp16/fp32 weight, it stashes it in _nvfp4_pending_weight; process_after_loading then calls quantize_loaded_weight to pack it. The 128x4 layout quantizes through FlashInfer’s nvfp4_quantize, which needs CUDA.
The forward pass: spec_id picks the kernel
Layers do not write fp8 / cutlass / marlin branches themselves.LinearBase.forward hands spec.spec_id to the dispatcher, which picks a kernel:
layers/linear/dispatch.py) keys its cache on (spec_id, M_bucket, N, K, in_dtype, out_dtype, sm, mode), so decode (small M) and prefill (large M) can land on different kernels. Each spec’s spec_id:
Extension
The system leaves two extension points, one per kind of change.1
Add a framework: write an importer
Implement This touches config parsing only; the semantic and physical layers stay untouched.
detect and build_plan (the QuantImporter Protocol in layers/quant/importers/base.py), then add it to DEFAULT_IMPORTERS in registry.py:2
Add a precision: write a spec and wire materialize
First add the type to
QDType (it is probably already there), then implement a spec satisfying WeightSpec: allocate for the weight and scales, spec_id for a unique id, and process_after_loading plus the activation-quant hook if needed. Then wire the matching QDType to that spec in materialize, and finally register a LinearKernel for its spec_id.
