CERIN AMROTH · ML systems

Why does `load_in_4bit` still OOM on a Mixture-of-Experts model?

Solved by experts4bit-qlora · this page in the repository (pinned 0c2a256dcdc2, the source of this rendering; latest on main, unpinned; not the source of any fact rendered here)

Install routes

From docs/capabilities.json at the pinned commit.

pip install "experts4bit-qlora[train]"

Primary route: streaming loader + trainer.

Environment (from the capability register): OS: Linux · Python: >=3.11 tested in CI (pyproject says >=3.9; 3.9/3.10 are not tested) · Accelerator: NVIDIA CUDA GPU (bitsandbytes 4-bit) · Requires: bitsandbytes>=0.43; torch>=2.2; transformers>=5.0 for the streaming loader ([train] extra)

Because bitsandbytes' 4-bit walker only replaces nn.Linear, and transformers v5 stores a MoE's experts as one fused 3-D nn.Parameter per layer, so the experts — most of the weights — are silently left in bf16. experts4bit-qlora quantises exactly that fused stack on the way to the GPU, and verify_moe_4bit(model, strict=True) proves it happened.

Symptoms

Why it happens

transformers v5 fuses each MoE layer's experts into two stacked tensors, gate_up_proj [E, 2I, H] and down_proj [E, H, I]. the bitsandbytes 4-bit walker that from_pretrained(..., quantization_config=BitsAndBytesConfig(load_in_4bit=True)) runs looks for nn.Linear modules; a 3-D parameter is not one, so it is skipped without a warning (bitsandbytes#1849). The dense side shrinks, the experts do not, and the experts are the overwhelming majority of a MoE's bytes. Some checkpoints are worse: a naive from_pretrained of an MXFP4 release such as DeepSeek-V4 materialises the fp4 experts to bf16 first (../DEEPSEEK-V4.md).

Which project solves it

experts4bit-qlora owns loading and quantisation orchestration. Its Experts4bit primitive (the 4-bit face of ExpertsNbit; nf4 / fp4 / int8 / fp8 / bf16 / fp16 storage) is frozen quantised storage for a fused expert stack, with quantisation blocks that never cross an expert boundary. load_moe_4bit_streaming streams the checkpoint tensor-by-tensor onto the GPU, quantises each expert stack as it arrives and drops the bf16 source, so the bf16 model is never materialised in CPU or GPU RAM. verify_moe_4bit is the read-only check that works on any model, including one loaded the stock way. No kernel package is needed for this step; grouped-nf4-gemm only enters when you want the fused GEMM (qlora-fused-moe-experts.md). Relationship to bitsandbytes and the upstream PR: ../BITSANDBYTES.md.

Install

pip install "experts4bit-qlora[train]"   # the streaming loader needs transformers>=5.0

e4b, e4b-qlora, experts4bit, expertsnbit and experts-mxfp4 are lookup aliases of the same package; install the canonical name.

Smallest correct example

Needs: GPU + network + model download.

import torch
from experts4bit_qlora import load_moe_4bit_streaming, verify_moe_4bit

model, config = load_moe_4bit_streaming(
    "Qwen/Qwen3-30B-A3B", "cuda", torch.bfloat16, r=8, alpha=16, quant_type="nf4",
)
report = verify_moe_4bit(model, strict=True)   # raises if any expert stack is still bf16
print(report["n_quantized"], report["n_unquantized"])

To load a pinned checkpoint pass revision=<commit sha>: it reaches both the config and the snapshot lookup, so a snapshot staged at that sha loads offline as-is, no hand-written refs/main — held by a regression test that forces HF_HUB_OFFLINE over a staged hub cache with no refs/ and drives the real transformers/huggingface_hub resolution. The receipt is config._commit_hash: the commit actually loaded, filled from the snapshot folder when transformers left it empty. A snapshot that resolves to a different commit is refused rather than loaded.

Needs: CPU-only. The same check on a model you loaded some other way:

from experts4bit_qlora import verify_moe_4bit

report = verify_moe_4bit(stock_model)          # e.g. from_pretrained(..., load_in_4bit=True)
for stack in report["unquantized"]:
    print(stack["module"], stack["dtype"], stack["shape"])   # the stacks bitsandbytes skipped

Expected result

verify_moe_4bit returns {"quantized": [...], "unquantized": [...], "n_quantized": int, "n_unquantized": int}. After the streaming loader, n_unquantized == 0 and every quantized entry reports quant_type == "nf4" (or the quant_type you passed). On a stock 4-bit load, unquantized lists each ...experts.gate_up_proj with its bf16 dtype and 3-D shape, and strict=True raises RuntimeError naming the count and the fix. The loader itself refuses to return a model on which it quantised zero expert layers.

Supported scope

Limitations

Evidence

Register: ../claims.json. Status words as in ../STATUS.md.

Common wrong approaches

Source and freshness

This page is a rendering of docs/solutions/bitsandbytes-moe-load-in-4bit-still-ooms.md at commit 0c2a256dcdc2 (sha256 b66b7e2008d3bbd7…). Numbers are never copied here: every measured statement cites a claim ID in docs/claims.json at that commit. Repository-relative links resolve to this site's pages where the document is published and to the pinned commit otherwise.

CURRENT · source: pjordanandrsn/experts4bit-qlora@0c2a256dcdc2 · rendered package: 0.35.3 · latest published package: 0.35.3