Instructions to use mncai/nf4moe with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use mncai/nf4moe with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
- QLoRA for a 743B Fused-MoE: Quantizing the 3D Expert Tensors No Library Reaches (Yet)
- The wall: three formats, zero training paths
- Why every quantizer misses 95% of the model
- The fix: dequant-on-forward for routed experts (β120 lines)
- Does it actually train?
- Making it fast: the step tax and token-budget batching
- Practical gotchas (the part I wish someone had written for us)
- What this is β and isn't
- Links
- The wall: three formats, zero training paths
QLoRA for a 743B Fused-MoE: Quantizing the 3D Expert Tensors No Library Reaches (Yet)
How we NF4-quantized GLM-5.2's fused MoE experts by hand β β120 lines of code β to fit a 743B frozen base on a single node and train through it.
TL;DR β Modern
transformersMoE checkpoints store experts as fused 3Dnn.Parameters, notnn.Linearmodules. Every off-the-shelf weight-only quantizer (bitsandbytes, torchao, AWQ, GPTQ) works by module swap, so they silently skip the experts β which are 95% of the parameters. Result: you cannot QLoRA a big fused-MoE today with stock tooling; bitsandbytes tracks this as issue #1849, and the official fix (PR #1965,Experts4bit) is still open at the time of writing. We wrote a small behavior-preserving drop-in module that stores each expert in NF4 and dequantizes only the routed experts on the fly in forward. It shrinks GLM-5.2 from β1.49 TB to β414 GB, keeps the frozen base differentiable (gradients flow through to a trainable projector/LoRA), and trained a full VLM program on 8ΓB200 β including a 5.5Γ throughput win from token-budget batching on top. Code: github.com/genonai/nf4moe (Apache-2.0).
The wall: three formats, zero training paths
We wanted to train a vision projector + LoRA on top of a frozen GLM-5.2 (743B-total / 39B-active MoE). The projector sits at the LLM's input (visual embeddings spliced into the token sequence), so the loss gradient must backpropagate through the entire frozen base to reach it. That imposes two hard requirements: the base must (a) fit on the node, and (b) stay differentiable.
Here is the whole training structure β solid arrows are the forward pass, the dashed arrow is the gradient's return trip. Only the blue boxes train; the dark-blue expert stack is what this post is about:
π΅ blue = trained (bf16) Β· β¬ gray = frozen bf16 Β· π· dark blue = frozen NF4 β the 95% no stock quantizer reaches
| Format | Fits 8ΓB200 (β1.46 TB)? | Backward? |
|---|---|---|
| bf16 original (β1.49 TB) | β | β |
| FP8 serving checkpoint | β | β β block-FP8 matmul has no backward kernel |
| 4-bit weight-only (bnb / torchao / AWQ / GPTQ) | β experts stay bf16 β CPU spill | β |
The third row is the surprising one, and it's not a bug in any one library.
Why every quantizer misses 95% of the model
GLM-5.2's MoE block (GlmMoeDsaNaiveMoe) stores its 256 experts per layer as two fused 3D tensors:
gate_up_proj: nn.Parameter # [256, 2*2048, 6144]
down_proj: nn.Parameter # [256, 6144, 2048]
# used as: F.linear(x, gate_up_proj[expert_idx])
There is no nn.Linear here β just raw 3D parameters indexed per expert. Weight-only quantizers are all nn.Linear module-swap designs, so they quantize the attention and dense layers (β39B params) and walk right past the experts (β700B params, 95% of the model). Hand them NVFP4 or NF4 config and you get a "quantized" model that is still β1.4 TB.
This is an ecosystem-wide, acknowledged gap, not our diagnosis:
- bitsandbytes #1849 (open) β transformers v5's fused-expert layout broke the 4-bit path. The issue's own demo: Qwen3-30B-A3B loaded "in 4-bit" takes 55.6 GB instead of the expected β15 GB (quantization silently skipped β the same symptom we hit as CPU spill).
- Unsloth docs β "Training MoE in 4-bit QLoRA isn't recommended β BitsandBytes doesn't support it. This isn't specific to Unsloth." Their recommendation is bf16 LoRA β which for 743B does not fit on a single node.
- torchao β has MXFP8 MoE training building blocks (grouped GEMM, differentiable), but that's 8-bit full-training infrastructure, not 4-bit weight-only QLoRA.
- Inference-side 4-bit fused-MoE exists (llama.cpp/GGUF, vLLM Marlin/AWQ/GPTQ) β but none of it is differentiable. The QLoRA-training Γ fused-MoE intersection was empty.
The fix: dequant-on-forward for routed experts (β120 lines)
The key observation is that QLoRA never needed anything fancy from the base weights: they're frozen, so we never need their gradients β only gradients through them, with respect to the activations. F.linear(x, W) is differentiable in x regardless of where W came from; the dequantization is a constant w.r.t. the activations. So the standard QLoRA trick ports directly to 3D tensors β someone just has to write the module.
We replaced each GlmMoeDsaNaiveMoe with a drop-in that keeps each expert's two 2D slices as NF4 packed buffers (bitsandbytes functional API β quantize_4bit / dequantize_4bit, blocksize 64 β no nn.Linear needed) and dequantizes only the experts the router actually hit (condensed here β the real module is β120 lines):
class QuantizedNaiveMoe(nn.Module):
"""Behavior-preserving drop-in for GlmMoeDsaNaiveMoe, experts held as nf4."""
def _deq(self, packed, state):
# constant w.r.t. activations β F.linear(x, w) stays differentiable in x
return bnbF.dequantize_4bit(packed, state, quant_type="nf4").to(self.compute_dtype)
def forward(self, hidden_states, top_k_index, top_k_weights):
final = torch.zeros_like(hidden_states)
with torch.no_grad():
expert_mask = F.one_hot(top_k_index, self.num_experts).permute(2, 1, 0)
# one .tolist() sync instead of thousands of per-expert GPUβCPU stalls per step
expert_hit = expert_mask.sum(dim=(-1, -2)).gt(0).nonzero().flatten().tolist()
for e in expert_hit:
top_k_pos, token_idx = torch.where(expert_mask[e])
x = hidden_states[token_idx]
gate, up = F.linear(x, self._deq(self.gup_packed[e], self.gup_state[e])).chunk(2, -1)
h = F.linear(self.act_fn(gate) * up, self._deq(self.dn_packed[e], self.dn_state[e]))
final.index_add_(0, token_idx, h * top_k_weights[token_idx, top_k_pos, None])
return final
Two details that matter in practice:
QuantStatetravels with the module. bnb's quant state (absmax, nested code) is not a buffer, so.to(device)won't move it. Override_applyto run the same function over each state's tensors β otherwise you get cross-device crashes the first time accelerate moves a layer.- Batch the host sync. The naive per-expert
int(tensor)loop costs thousands of GPUβCPU syncs per step across 256 experts Γ all MoE layers. One.tolist()on the hit-mask is numerically identical and removes the stall.
Loading 1.4 TB without OOMing anything
The load path matters as much as the module (load_quant.py):
from_pretrainedβ bf16 on CPU (a 2 TB-RAM host holds it; zero GPU pressure).- Walk the decoder layers: quantize each layer's experts directly onto their target GPU (round-robin), then drop the bf16 3D tensor immediately β CPU memory frees incrementally as GPUs fill.
- Move each layer's remaining bf16 params (attention, dense MLP, shared experts, router) to the same GPU.
accelerate.dispatch_modelwith the assembleddevice_mapadds the cross-device hooks.
Resulting footprint:
| Component | Params | bf16 | nf4moe |
|---|---|---|---|
| Fused experts | β700B | β1.40 TB | β336 GB |
| Everything else (attn, dense, shared experts, router, embed, head) | β39B | β78 GB | β78 GB (kept bf16) |
| Total | 743B | β1.49 TB β doesn't fit | β414 GB β 55β64 GB/GPU on 7 GPUs |
At β414 GB, one replica actually fits on 4 GPUs (β103 GB/card) β which is what let us run 2-replica data parallelism on the 8-GPU node later.
Checkpointing the quantized model
A plain state_dict() silently drops bnb's QuantState β a reload gives garbage. We serialize each MoE module's packed bytes plus QuantState.as_dict(packed=True) explicitly, sharded per module. Payoff: subsequent runs skip the β100-minute bf16 read + re-quantization and load NF4 directly (β3Γ faster cold start). Corollary: never let HF Trainer checkpoint the sharded quantized base (save_strategy="no" + a projector-only save callback) β it will either crash on the cross-device gather or write a broken multi-hundred-GB checkpoint.
Does it actually train?
Verification ladder, cheapest first:
- Synthetic MoE parity β nf4 vs bf16: output rel-err 0.15 (expected for NF4), gradient cosine 0.99, 3.56Γ memory reduction.
- Small real-class model, 2 GPUs β accelerate-dispatched forward+backward,
inputs_embedsgradient present and finite. - Real 743B smoke β β414 GB sharded with zero CPU spill; forward loss 6.80 (finite); projector grad-norm 405.8, finite, through the entire frozen quantized base.
On top of this base we then ran a month-long VLM program: stage-1 projector alignment (val loss 10.82 β 3.78 on LLaVA-Pretrain), stage-2 SFT with LoRA on attention + shared experts (best MMStar 58.87 for a frozen-base transplant), LoRA merges back into the FP8 checkpoint for vLLM serving at β97 tok/s, and even LoRA applied directly to the fused 3D experts (per-expert A/B on the 3D stacks β a lever no stock PEFT path offers either). The quantization was never the bottleneck again.
Making it fast: the step tax and token-budget batching
One non-obvious property of dequant-on-forward at this scale: each training step pays a roughly fixed β2.5 s dequantization tax (every hit expert dequants once per step, roughly independent of batch size). With short SFT rows (median β438 tokens) and small batches, that tax dominates the step.
The fix is embarrassingly simple: batch to a token budget, not a row count. Packing β27 short rows per step amortizes the fixed tax and multiplies throughput:
Measured end-to-end on real training (β10 h runs): 5.5Γ over the original pipeline, 3Γ over plain 2-replica data parallelism. Micro-batching, by contrast, moves in the wrong direction under a fixed per-step tax β we measured that too.
Practical gotchas (the part I wish someone had written for us)
- PEFT target-module matching can silently drop modules on custom architectures (we hit peft 0.19.1 dropping MLP targets on the quantized
glm_moe_dsa). Force-match and then assert the trainable-parameter count β a LoRA that attaches to nothing trains "successfully" and learns nothing. Apply the same check at eval-time adapter load. - NF4 quantization requires CUDA (bnb kernels) β plan the load path around it.
- Pin your stack: we validated on torch 2.11.0+cu128 β torchvision 0.26.0, bitsandbytes 0.49, torchao 0.17. A torch downgrade breaks
torchvisionimport, which takestransformers.AutoProcessordown with it. - On a shared box, the bf16 CPU read is disk-bound: β41 min quiet, 2.5Γ worse under contention. The NF4 checkpoint reload (previous section) is what makes iteration tolerable.
What this is β and isn't
Honest positioning: this is not a new quantization method. It is QLoRA (Dettmers et al., 2023) applied to fused 3D expert tensors β conceptually obvious, and the reason it works (dequant is constant w.r.t. activations) is one sentence. The contribution is timing and evidence: the mainstream stack could not do this at the time of writing, the gap is acknowledged in the libraries' own trackers, and we show it working at 743B scale on one node, with the load path, checkpoint format, sync patterns, and throughput fixes that turn the one-liner into a usable training setup.
The window is closing by design: when bitsandbytes lands Experts4bit (PR #1965) and frameworks adopt it, this becomes table stakes β which is exactly what should happen. Until then, if you need to QLoRA a big fused-MoE, the recipe above is β120 lines and works today. If you're building the official version: the QuantState device handling, the routed-expert host-sync batching, the Trainer-checkpoint hazard, and the fixed per-step dequant tax (β token-budget batching) are the four things we'd want the docs to mention.
Links
- bitsandbytes issue #1849 Β· PR #1965 (Experts4bit)
- QLoRA: Dettmers et al., 2023
- Code: github.com/genonai/nf4moe β
quant_moe.py(the drop-in module),load_quant.py(OOM-free loader/dispatch + NF4 checkpointing),tests/smoke_nf4moe.py(real-743B validation). Apache-2.0, self-contained. - The broader research program this enabled (vision transplant onto the frozen 743B, and where its ceiling turned out to be) is written up separately.
Experiments ran JuneβJuly 2026 on a single 8ΓB200 node. Library states (bnb #1849/#1965 open, Unsloth guidance, torchao scope) were re-verified on 2026-07-22.
- Downloads last month
- -

