Runtime configuration
Canonical reference for every attnquant environment variable and config.json field, including the fast decode path and NVFP4 quantization.
AttnQuant reads its behavior from two places: config fields in the model’s config.json, and environment variables that override those fields at runtime. Environment variables win. They apply to both the decomposition adapter and the served model graph, so the theta bundle and the served weights stay consistent. This is the one place to look up a flag or field; other pages link here rather than restate them.
If you serve on an NVIDIA GPU, start with these settings:
- Set
ATTNQUANT_BLOCKSto the model’s layer count (26 for Gemma 3 1B, 60 for Gemma 4 31B) for the passthrough operating point, where output matches the original model. The config default is1, which is an analysis setting, not a serving setting. - Set
ATTNQUANT_PRECOMPUTE_OUTSIDE=1for the fastest decode. - Leave
ATTNQUANT_GEMMunset so device-aware selection picks the GEMM path. - Add
ATTNQUANT_QUANT=nvfp4for 3.18x smaller whole-model weights on large models.
Environment variables
Each variable below overrides the corresponding config field for both decomposition and serving:
| Variable | Default | Effect |
|---|---|---|
ATTNQUANT_PRECOMPUTE_OUTSIDE |
0 (recommended: 1) |
Build the effective weight W_eff once at load time, outside the graph, so each projection is a single matmul. The recommended fast decode path. See below. |
ATTNQUANT_QUANT |
unset | Quantize W_eff at load time on the precompute-outside path. Supported value: nvfp4 (4-bit weights with per-block float8 scales, 3.18x smaller whole-model). See below. |
ATTNQUANT_RANK |
config lora_rank (32) |
Rank r of the low-rank U, V delta. Higher rank fits the original weight more closely and stores more data per projection. |
ATTNQUANT_BLOCKS |
config shared_blocks (1) |
Number of contiguous layer bands G, each with its own W0. Set to the layer count for the passthrough serving point. Values below 1 are rejected; values above the layer count clamp to it. See banded sharing. |
ATTNQUANT_GEMM |
unset (auto) | Force the execution path: 1/true for GEMM, 0/false for the custom op. Unset is device-aware (GEMM on NVIDIA and other non-Metal accelerators, custom op on Metal at low rank). The custom op supports rank up to 64; use GEMM above that. The selection algorithm lives in execution paths. |
ATTNQUANT_TIE_FAMILIES |
config tie_families ("all") |
Which projection families are attnquant: all (every projection) or attn (only q/k/v/o; the MLP stays dense). Applies to both the adapter and the model. |
ATTNQUANT_QKV_FUSE |
1 |
Fuse the Q, K, and V projections into one matmul on the GEMM path. Set 0 to disable. No effect on the custom-op path. |
ATTNQUANT_FUSED_MLP |
1 |
Fuse gate, up, activation, and down into one generated_mlp custom op. Custom-op path only. Set 0 to disable. |
ATTNQUANT_FUSE_LORA |
0 |
Fold the LoRA U, V delta into a single effective-weight matmul. GEMM path only. |
ATTNQUANT_PRECOMPUTE |
0 |
Precompute W_eff inside the graph, recomputing U Vᵀ on every forward pass. GEMM path only. Superseded by ATTNQUANT_PRECOMPUTE_OUTSIDE for decode. |
ATTNQUANT_MEF_CACHE_DIR |
~/.cache/attnquant/compiled_graphs/ |
Directory for cached compiled graphs. Set to an empty string to disable caching. |
ATTNQUANT_PRECOMPUTE and ATTNQUANT_PRECOMPUTE_OUTSIDE are different flags, so set the one you mean. ATTNQUANT_PRECOMPUTE builds W_eff inside the graph and recomputes the delta on every forward pass. ATTNQUANT_PRECOMPUTE_OUTSIDE builds it once at load time and never recomputes it, which is what you want for decode.
ATTNQUANT_PRECOMPUTE_OUTSIDE
When set to 1, each layer’s effective weight W_eff = s · W0 + U Vᵀ is materialized once at load time, outside the graph (see execution paths). Every projection then runs a single x @ W_effᵀ matmul, the same launch count as the built-in dense path, with Q/K/V fused into one matmul. That takes the path from 21 kernel launches per layer to 8. This is the recommended setting on NVIDIA GPUs.
If a layer’s theta keys can’t be matched to a shape- and band-compatible W0 at load, the precompute fails immediately with the offending layers listed. That almost always means ATTNQUANT_BLOCKS differs between decomposition and serving. Set it once on the command line so both stages read the same value.
Peak GPU memory on this path in bf16 measures about 1.9x the built-in baseline (32.7 vs 17.6 GB on Gemma 3 1B). ATTNQUANT_QUANT=nvfp4 recovers most of that by shrinking the weights 3.18x.
To serve on the fast path at passthrough:
ATTNQUANT_BLOCKS=26 ATTNQUANT_PRECOMPUTE_OUTSIDE=1 \
pixi run python run_max.py serve \
--model google/gemma-3-1b-it \
--custom-architectures attnquant \
--port 3100
run_max.py registers attnquant as a MAX custom architecture and forwards everything after the subcommand to max, so --max-length, --device-memory-utilization, and other max serve flags pass straight through.
In-flight batching is on by default
run_max.py appends --enable-in-flight-batching to serve unless you have
already decided: an explicit --enable-in-flight-batching or
--no-enable-in-flight-batching, or a --config-file, always wins.
MAX ships this off (enable_in_flight_batching defaults to False), which
makes context encoding and token generation mutually exclusive. An arriving wave
of requests then drains through several prefill-only iterations while decode
stalls, and every request in the wave pays that drain in its time-to-first-token.
Measured on one B200, Gemma 4 31B NVFP4, medians over 48 trials per arm across two independent sessions:
| Concurrency | TTFT p50 off | TTFT p50 on | Aggregate tok/s off → on | ITL off → on |
|---|---|---|---|---|
| 1 | 33.7 ms | 33.8 ms | 91.4 → 90.9 | 10.76 → 10.81 |
| 8 | 77.4 ms | 53.9 ms | 655.3 → 669.5 | 11.60 → 11.50 |
| 16 | 127.8 ms | 76.8 ms | 1240.7 → 1257.4 | — |
| 32 | 235.7 ms | 106.8 ms | 2107.2 → 2151.3 | 13.04 → 12.98 |
| 64 | 434.5 ms | 220.5 ms | 3248.5 → 3227.7 | 15.84 → 16.00 |
It’s a median-latency fix. TTFT p50 falls 49.2% at c=64 and 54.7% at c=32, while aggregate throughput and inter-token latency stay flat within about 1%.
The tail isn’t improved. p99 TTFT moves −6.7% at c=64 and +2.6% at c=32, so a service level written against p99 rather than p50 doesn’t see this flag.
At concurrency 1 there’s never a decode batch for prefill to join, so the flag makes no difference; it matters only if you serve concurrent traffic.
ATTNQUANT_QUANT
When set to nvfp4 (with ATTNQUANT_PRECOMPUTE_OUTSIDE=1), attnquant quantizes each W_eff to NVFP4 at load time: packed 4-bit weights with per-block (1, 16) float8 scales instead of bf16. Whole-model weight bytes shrink 3.18x (on Gemma 4 31B, from 61.395 GB in bf16 to 16.474 GB of NVFP4 plus 2.819 GB of unquantized bf16 embeddings and tied lm_head), and decode measures 100.6 tok/s on one B200, +9.4% over the built-in NVFP4 path’s 92.0 tok/s (max serve + max benchmark, concurrency 1, /v1/chat/completions). It also reads 1.63x fewer bytes per token than the built-in path, which keeps all 60 self_attn blocks in bf16. The tied lm_head is bf16 on both paths, so the difference is attention coverage alone, and that coverage is where the throughput comes from, at a cost of about 0.41 pp of MMLU-Pro. Set ATTNQUANT_QUANT_IGNORE=self_attn to decline the trade, which also removes the speedup.
Quantization runs once at load (the measured first load for the 31B model’s 410 projections was ~51 minutes), and the quantized state dict is cached to disk under ~/.cache/attnquant/quantized_weights/, which cut the next load to ~22 minutes:
ATTNQUANT_BLOCKS=60 ATTNQUANT_QUANT=nvfp4 ATTNQUANT_PRECOMPUTE_OUTSIDE=1 \
pixi run python run_max.py serve \
--model google/gemma-4-31B-it \
--custom-architectures attnquant \
--port 3100
NVFP4 is a footprint win first: the 1.90x cut in bytes moved per decode step buys only 1.19x on the serve path, because 47% of a profiled step is byte-invariant and the NVFP4 kernels themselves reach only 47% of peak bandwidth. See GPU acceleration.
Config fields
The attnquant config class reads these from config.json and inherits them alongside every standard field of the base architecture. Defaults are backward compatible, so an unmodified Gemma repo (whose config.json doesn’t carry these keys) loads with sensible values:
| Field | Default | Description |
|---|---|---|
lora_rank |
32 |
Rank r of the per-(band, family) LoRA delta. Overridden by ATTNQUANT_RANK. |
tie_families |
"all" |
"all" makes every projection attnquant. "attn" restricts it to the q, k, v, and o families and leaves the MLP dense. Overridden by ATTNQUANT_TIE_FAMILIES. |
shared_blocks |
1 |
Number of layer bands G. Overridden by ATTNQUANT_BLOCKS. |
tied_projections |
all seven families | Read-only: the families implied by tie_families, derived at initialization. Setting it has no effect; change tie_families instead. |
ATTNQUANT_RANK and ATTNQUANT_BLOCKS must match between decomposition and serving. Because both stages read the same environment variables, setting them once on the command line keeps the theta bundle and the model graph in sync.