Skip to content

fix(models): better support for mixed-precision compressed-tensors NVFP4 - #390

Open
Sam-Izdat wants to merge 1 commit into
FlashML-org:mainfrom
Sam-Izdat:pr/mixed-precision-nvfp4
Open

fix(models): better support for mixed-precision compressed-tensors NVFP4 #390
Sam-Izdat wants to merge 1 commit into
FlashML-org:mainfrom
Sam-Izdat:pr/mixed-precision-nvfp4

Conversation

@Sam-Izdat

Copy link
Copy Markdown

Summary

Adds support for compressed-tensors checkpoints that mix per-tensor
FP8 attention/shared-expert projections with NVFP4 routed
experts - a layout produced by llm-compressor and found in the wild.

Without this, such checkpoints either crash on load (FP8 attn gets
torch.cat'd with bf16 weights) or produce garbled output (the dequant
kernel multiplies by the per-row global scale, but llm-compressor
stores the QUANT-side scale rather than the DEQUANT-side divisor that
modelopt stores).

The fix is in five files, +236/-63 lines. The key idea is to use the
on-disk tensor naming as the ground truth for which convention is
in use (weight_packed -> llm-compressor, weight_scale_2 -> modelopt)
rather than relying on the config-side format: nvfp4-pack-quantized
string (which both exporters set).

Test models

HF repo Layout Result
nvidia/Qwen3.6-35B-A3B-NVFP4 FP8 attn + NVFP4 shared-expert + NVFP4 experts (modelopt mixed_precision) regression-clean, coherent
primitive-ai/Ornith-1.5-35B-A3B-agentic-NVFP4-FP8 FP8 attn + FP8 shared-expert + NVFP4 experts (compressed-tensors mixed-precision) coherent (was: garbled output)

Tested with --moe-backend offload --expert-load parallel on RTX 3060
12 GB (nvfp4_backend='triton'). Ornith decoding at ~30-40+ tok/s with minimal tuning.

Fixed

  1. Crash on mixed-precision attn+shared-expert — the
    compressed-tensors iter doesn't know about FP8. Routing
    compressed-tensors MoE to the modelopt iter (which handles FP8
    attn) when the shared-expert is in the FP8 group avoids the
    cat-of-fp8-and-bf16 crash.

  2. Missing global-reciprocal for llm-compressor NVFP4 experts
    the dequant kernel multiplies by the per-row global; modelopt
    stores the DEQUANT-side divisor directly, but llm-compressor
    stores the QUANT-side scale that must be reciprocated. A
    per-checkpoint flag on the NVFP4 source spec picks the right
    convention.

  3. Unknown NVFP4 kind input_global_scale (parallel build)
    the bank dispatch only knew weight_scale_2 (modelopt). Alias
    weight_global_scale -> weight_scale_2 via spec.kind_map;
    skip input_scale / input_global_scale (activation scales,
    not bank tensors).

  4. Per-expert naming regex too narrow for parallel build — the
    expert key pattern only matched weight | weight_scale | weight_scale_2. Extended to cover the full llm-compressor
    per-expert naming set so the parallel bank builder's
    weight_info populates correctly.

  5. Iter dispatch routed mixed-precision MoE to the
    compressed-tensors iter
    , which holds more GPU state per-shard
    than the default iter (caused OOM on 12 GB). Reverted to the
    default iter for that case.

  6. The on-disk-naming heuristic for reciprocal was too coarse.
    format: nvfp4-pack-quantized is set by BOTH llm-compressor and
    modelopt re-exports. Now probes the safetensors index for
    weight_packed (llm-compressor signature) vs weight_scale_2
    (modelopt signature) and picks the convention from the data, not
    the config claim.

Files changed (5, +236/-63)

  • python/freetoken/models/config.py_nvfp4_global_reciprocal
    heuristic probing the safetensors index;
    ModelConfig.nvfp4_global_reciprocal field
  • python/freetoken/models/nvfp4_banks.py — bank kind dispatch
    handles weight_global_scale (via spec.kind_map) and skips
    activation scales; serial and parallel paths updated in lockstep
  • python/freetoken/models/qwen3_5_moe/config.py_has_moe_experts
    helper; _attn_quant and _expert_quant extended for
    compressed-tensors; dense_quant probe for FP8 shared-expert;
    parse_config wiring
  • python/freetoken/models/qwen3_5_moe/moe.py_SharedExpert
    refactored to use the quant_linear factory, with
    attn_quant='fp8_pertensor' dispatch for the mixed-precision
    shared-expert path
  • python/freetoken/models/qwen3_5_moe/weight.py — per-checkpoint
    _spec_for with kind_map; _load_maybe_quantized and
    _nvfp4_parts accept llm-compressor naming; _PT_FP8_FUSE and
    _CT_NVFP4_FUSE extended for shared-expert gate|up fuse; expanded
    _NVFP4_EXPERT_KEY_RE regex

Backwards compatibility

  • NVIDIA Qwen3.6-35B-A3B-NVFP4 (modelopt) is regression-clean.
    No NVFP4 reciprocal applied (the data shows weight_scale_2 only,
    which signals the modelopt convention).
  • The default iter (the one most production single-group NVFP4 MoE
    exports use) is unchanged in behavior.

Notes

The heuristic that disambiguates llm-compressor from modelopt
probes the safetensors index for a routed-expert tensor's sibling
suffixes. This is a one-time read at model-load time; no per-tensor
overhead. If both weight_packed and weight_scale_2 are absent
(e.g. some other compressed-tensors flavor), the heuristic falls
back to the safe default of "no reciprocal" same as
global_reciprocal=False upstream.

The compressed-tensors iter crashed on mixed-precision NVFP4
checkpoints (FP8 attention/shared-expert + NVFP4 routed experts) and
loaded the per-row global scale without reciprocating, producing
garbled output. Root cause: the iter and bank builder assumed
modelopt's per-expert naming and dequant-side-divisor convention;
llm-compressor and the same family use a different naming
(`weight_packed` / `weight_global_scale`) and store the
quant-side scale instead.

Fixes:
- Probe the safetensors index for `weight_packed` vs
  `weight_scale_2` to pick the right reciprocal convention per
  checkpoint (data, not config claim).
- Route compressed-tensors MoE to the modelopt iter when the
  shared_expert is in the FP8 group, avoiding the FP8+bf16 cat crash.
- Extend the NVFP4 expert key regex and bank kind dispatch to cover
  llm-compressor naming (`weight_packed` / `weight_global_scale`
  / `input_global_scale`).
- `_SharedExpert` refactored to use the `quant_linear` factory,
  with a new `attn_quant='fp8_pertensor'` dispatch for the
  mixed-precision path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant