How do I QLoRA-train the fused experts of a MoE (per-expert LoRA on 4-bit experts)?
Solved by experts4bit-qlora · this page in the repository (pinned 605d21e67958, 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: minimum/reference training.
Alternatives:
pip install "experts4bit-qlora[train,fast]"— loader plus accelerated grouped-kernel path
Environment (from the capability register; one list for all of these routes, so not every item applies to each): OS: Linux · Python: >=3.11 tested in CI (pyproject says >=3.10; 3.10 is 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)
Load with load_moe_4bit_streaming, which installs a frozen NF4 Experts4bit stack wrapped in trainable per-expert ExpertsLoRA adapters for every MoE layer, then train with python -m experts4bit_qlora.train or your own loop. In your own loop, add enable_fast_train(model, dgrad=True) from the [fast] extra to run the step through grouped-nf4-gemm's grouped kernels, and assert its return value; the trainer does the same with FAST_TRAIN=1 and otherwise trains through the per-expert reference loop.
The abstraction that fails, and what replaces it
PEFT expects nn.Linear targets. A fused MoE checkpoint stores each layer's experts as stacked 3-D projections — gate_up_proj [E, 2I, H] and down_proj [E, H, I] — so there is no per-expert Linear for target_modules to match. A delta hung on the stack as a whole would also compute the wrong function: the low-rank term has to be applied per routed expert before that expert's activation (act(W_e x + B[e] A[e] x), not act(W_e x) + d). ExpertsLoRA supplies the adapter abstraction — stacked per-expert A[e] / B[e] over the frozen packed base, the delta injected pre-activation — and grouped-nf4-gemm supplies the fused grouped forward and, with dgrad=True, the optional input-gradient kernel.
Ordinary PEFT: nn.Linear per projection <- LoRA A/B hooked on each Linear
Fused checkpoint: experts.gate_up_proj [E, 2I, H], experts.down_proj [E, H, I] (no per-expert Linear)
|
v
Experts4bit frozen packed base + ExpertsLoRA A[e] / B[e], one pair per expert
|
v
per-routed-expert delta, applied BEFORE the activation: act(W_e x + B[e] A[e] x)
|
v
grouped forward (grouped-nf4-gemm) + optional dgrad (enable_fast_train(model, dgrad=True))
| component | state | where it is decided |
|---|---|---|
packed expert base (Experts4bit, NF4 by default) | frozen | quant_type= on the loader; ExpertsLoRA.__init__ sets requires_grad=False on every base parameter |
per-expert A[e] / B[e] (ExpertsLoRA) | trainable; B is zero-initialised, so the delta starts at exactly zero | r, alpha on the loader; R, ALPHA in the trainer |
router (mlp.gate.weight) | frozen by default — TRAIN_ROUTER=0; TRAIN_ROUTER=1 trains it at 0.1× the LoRA learning rate | its own trainer switch, separate from TRAIN_EXPERTS and TRAIN_ATTENTION; the trainer selects parameter names ending in mlp.gate.weight, ExpertsLoRA itself never touches the router, and the placement ablation found training it hurts (e4b.train.ablation) |
attention LoRA (LoRALinear over the frozen q/k/v/o) | trainable by default (TRAIN_ATTENTION=1) | a trainer switch |
| expert execution | reference per-expert loop (default), batched fallback (enable_batched_train), or fused grouped path (enable_fast_train, optional dgrad=True) | your call after loading; train.main() enables neither for you |
| base identity | verified, not assumed | verify_moe_4bit(model, strict=True) at load; the separate fused-train gate checked frozen bytes with a nonempty hash and a byte-flip control (see the distinction below); python -m experts4bit_qlora.infer serves the adapter over the same NF4 base it was trained against |
Symptoms
- "QLoRA a Qwen3 MoE on a 24 GB GPU" / "fine-tune fused MoE experts with LoRA on a consumer GPU".
- "experts4bit-qlora vs Unsloth for MoE QLoRA" / "is Unsloth faster than e4b on a 4-bit MoE" — the measured result below, one identical problem on one box.
- PEFT does not see the expert parameters:
target_modulesmatches nonn.Linearundermlp.experts, because the experts are a fused 3-D tensor (gate_up_proj,down_proj). - Stock
load_in_4bitleaves the experts in bf16 and the run OOMs (bitsandbytes-moe-load-in-4bit-still-ooms.md). - Training works but every step crawls through a per-expert Python loop.
enable_fast_trainreturned0(or something falsy) and nothing changed.- You need loss parity with a bf16 run, not just a falling curve.
Why it happens
Adapter libraries attach low-rank deltas to nn.Linear. A fused expert stack has no per-expert Linear to hook, and the adapter must land before the nonlinearity for each routed expert. ExpertsLoRA therefore re-implements the expert math: for each expert e the frozen 4-bit projections get a trainable scaling * (x @ A[e].T) @ B[e].T term, with stacked A/B per expert, while the base's projection re-dequantises in backward so no dequantised-expert activation is held (experts4bit_qlora/lora.py, ../BITSANDBYTES.md). The reference forward is a per-expert loop by design; the speed comes from the kernel side.
Which project solves it
experts4bit-qlora owns the adapter (ExpertsLoRA), the streaming loader, the trainer and the training-side offload. grouped-nf4-gemm (GitHub, PyPI) owns the kernels: enable_fast_train routes the ExpertsLoRA training forward through its nf4_qlora.fused_grouped_lora, and dgrad=True routes the backward through its single-launch dgrad kernel. [fast] is the seam. enable_batched_train is the no-extras fallback when the kernel package will not build.
Install
pip install "experts4bit-qlora[train]" # minimum/reference training: loader + trainer
pip install "experts4bit-qlora[train,fast]" # complete install for the accelerated example below
Smallest correct example
Needs: Linux, a CUDA GPU supported by the kernels, enough host RAM for the offloaded checkpoint, network access and the model download. Install [train,fast] above. This is a one-step expert-adapter smoke example; it is not a convergence run or a reproduction of the benchmark.
import torch
from transformers import AutoTokenizer
from experts4bit_qlora import enable_fast_train, 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", offload=True, # offload=True: experts in pinned host RAM, one layer on the GPU
)
verify_moe_4bit(model, strict=True)
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False}) # checkpointing is required with offload
n = enable_fast_train(model, dgrad=True)
assert n > 0, "still on the per-expert loop: grouped-nf4-gemm missing?"
# Match the trainer's expert-only parameter selection; freeze attention,
# router and all base weights. The loader has already attached expert LoRA.
trainable = []
for name, parameter in model.named_parameters():
is_expert_lora = "experts" in name and "lora" in name
parameter.requires_grad_(is_expert_lora)
if is_expert_lora:
trainable.append(parameter)
assert trainable, "no expert adapters to train"
optimizer = torch.optim.AdamW(trainable, lr=1e-4)
model.config.use_cache = False
model.enable_input_require_grads()
model.train()
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-30B-A3B")
batch = tokenizer("A telescope collects light from distant objects.", return_tensors="pt").to("cuda")
optimizer.zero_grad(set_to_none=True)
loss = model(**batch, labels=batch["input_ids"].clone()).loss
assert torch.isfinite(loss).item(), "non-finite training loss"
loss.backward()
grads = [parameter.grad for parameter in trainable if parameter.grad is not None]
assert grads and all(torch.isfinite(grad).all().item() for grad in grads), "missing or non-finite adapter gradients"
optimizer.step()
print(f"one expert-adapter step completed; loss={loss.item():.4f}")
# This smoke example does not save a trained adapter. Use the trainer below
# for dataset loading, evaluation, schedules and adapter checkpoints.
The assertion is there because acceleration must be observed, not assumed: enable_fast_train returns 0 rather than raising when the kernel package is absent, and from the caller's side 0 looks exactly like the per-expert loop it was meant to replace. Keep the assert in your own loop.
The shipped trainer provides dataset training (tatsu-lab/alpaca, response-only loss) and adapter checkpoints from environment variables (there are no CLI flags; --help prints them). It runs the per-expert reference loop unless FAST_TRAIN=1 (the fused path, enable_fast_train(model, dgrad=True), refused when it patches nothing); LAYER_MAJOR=1 implies it. Without MODEL, it and infer default to allenai/OLMoE-1B-7B-0924, not the model above; set MODEL= on both to change it:
STEPS=150 R=8 TRAIN_EXPERTS=1 OFFLOAD_EXPERTS=1 OUT=./out python -m experts4bit_qlora.train
Expected result
verify_moe_4bit(model, strict=True) returns without raising. enable_fast_train returns the number of ExpertsLoRA modules patched — one per MoE layer — and 0 means you are silently on the reference loop (../../CONTRIBUTING.md explains why every enabler returns a count). The trainer writes the adapter (adapter_best.pt) under OUT; python -m experts4bit_qlora.infer serves it over the same NF4 base with ADAPTER=./out/adapter_best.pt.
Supported scope
- Families, per path and from evidence — the capability's
model_families(olmoe,qwen3_moe,gemma4_text,mixtral,granitemoe) is exactly the families whosefast_traincell below issupported:
| model_type | quantize | reference_train | fast_train (the headline path) | batched_train | nvme_train | native_mxfp4_train |
|---|---|---|---|---|---|---|
olmoe | supported (tp1) | supported (tp1; e4b.train.olmoe-converges) | supported — tp1 OK · PASS on the registered text with real weights (e4b.train.parity.tp1.olmoe.fused.2026-09-05) | void — tp1 OK · VOID: the _PAD_WASTE_LIMIT fallback engaged without a counter (…olmoe.batched…) | not_tested (outside tp1; the arena ladder, e4b.offload.arena-vs-host-ram, is a separate receipt) | n/a |
qwen3_moe | supported (tp1, resident on a 32 GB card; flagship) | supported (tp1; flagship) | supported — tp1 OK · PASS resident on one 5090 (…qwen3.fused…), beside the flagship's five datasets (e4b.train.flagship-matrix) | void — tp1 OK · VOID: the kernel reached on a fraction of the layers every step (…qwen3.batched…); the dgrad-gate trajectory stands on its own fixture | not_tested (outside tp1; the arena ladder, e4b.offload.arena-vs-host-ram, is a separate receipt) | n/a |
gemma4_text | supported (tp1: the -it checkpoint loaded on this host, no #344; flagship: base) | supported (tp1; flagship) | supported — tp1 OK · PASS with the step-wise median inside the band by a small margin (…gemma4.fused…), beside the model-2 flagship (e4b.train.flagship-matrix) | void — tp1 OK · VOID: on some steps no layer reached the kernel (…gemma4.batched…) | not_tested (outside tp1; the arena ladder, e4b.offload.arena-vs-host-ram, is a separate receipt) | n/a |
granitemoe | supported (tp1: the first direct real-weight load) | supported (tp1 OK) | supported — tp1 OK · PASS on the corrected-counter re-run (…granite.fused…; attempt 1 a kept HARNESS_ERROR of the harness's counter, …granite.fused.attempt1…, amendment 3) — entered model_families on it | supported — tp1 OK · PASS (…granite.batched…) | not_tested | n/a |
gpt_oss | supported (bare GptOssExperts4bit; tp1) | refused — no ExpertsLoRA; attention-only QLoRA trains (…gptoss.attn_only…, OK · no pair) | refused — enable_fast_train returns 0 (…gptoss.fused…, REFUSED) | refused — enable_batched_train returns 0 (…gptoss.batched…, REFUSED) | refused — enable_mxfp4_nvme_residency refuses bias-carrying modules (#402; it had defaulted to the V4 epilogue, #397), enable_nvme_train_residency refuses bare modules, and the arena_train=True wrap is refused on structure | experimental — grouped-nf4-gemm's ExpertsMxfp4LoRA; tp1 canary and provenance passed on its own text (…gptoss.mxfp4…, EXPERIMENTAL); never licensed |
mixtral | supported (tp1: the first real-weight pass through the w1/w3/w2 fusion, offload=True) | supported (tp1, offload) | supported — tp1 OK · PASS under offload at half the reference loop's peak VRAM (…mixtral.fused…); entered model_families on this row | supported — tp1 OK · PASS, the kernel reached everywhere (the 8-expert shape; …mixtral.batched…) | not_tested | n/a |
Each cell is one of supported (completed under the registered protocol with a PASS/OK receipt), refused (with the reason), void (ran, unreadable), harness_error, not_tested, experimental, n/a — per path, never a flat flag; the machine-readable form, with the claim id behind every supported / void / refused cell, is training_support in ../capabilities.json, validated by scripts/check_capabilities.py, and model_families is exactly the families whose fast_train is supported. Row statuses in the tp1 receipt are one of OK / REFUSED / HARNESS_ERROR / ALARM / OOM / NOT_RUN / EXPERIMENTAL with the parity verdict (PASS / FAIL / VOID) as a separate column. On top of that table, the attention-4-bit configuration (TRAIN_ATTN_4BIT, reference_attn4/fused_attn4) has its own receipts since tp2/P40 (2026-09-06): supported on granitemoe, olmoe, qwen3_moe and mixtral (e4b.train.h2h.unsloth.<family>.5090.2026-09-06.arm.e4b.*, internal parity PASS on each); supported on gemma4_text since lane P67 (2026-09-24): its arms run since #435 (P56, tp4, P67; all 115 projections), and P67 measured the per-family floor band that tp1's constant band lacked (#713, e4b.train.p67.gemma4.fused-attn4.floor-band.5090.2026-09-24; tp2/P40's arms had died before a step on quantize_attention_projections_4bit converted 100 projections, expected 120; the bf16-attention fast_train cell above is unchanged); refused on gpt_oss (96 of 96 attention projections carry a bias, and the converter refuses bias-carrying projections). The per-family reasons live in training_support's path entries. DeepSeek-V4 is admitted by structure (the base supplies its clamped epilogue via _apply_gate, ../DEEPSEEK-V4.md) and has no training receipt. The per-family training table with every row's status is the tp1 section of ../ARCHITECTURE_SUPPORT.md; the loader's families are in bitsandbytes-moe-load-in-4bit-still-ooms.md.
- Storage: nf4 is the benchmarked QLoRA default; fp4 / int8 / fp8 / bf16 / fp16 carry a tested LoRA-step contract only (
../STORAGE-MODES.md). OFFLOAD_EXPERTS=1/offload=Trueis what makes a 30B-class MoE train on a 12 GB or 24 GB card (run-moe-larger-than-vram.md).- Double-quantized expert absmax, the default for resident training:
python -m experts4bit_qlora.trainapplies it by default since TC1 amendments 28 / 31 (1.4 % of the step for 1.34 GB on Qwen3-30B-A3B, 2.3 % for 2.04 GB on Mixtral-8x7B, held-out within 0.003), andenable_fast_traindoes since TC1 amendment 56 (packed 4,096-token rows: 1.002 of the step for 1.35 GB; one RTX 5090, torch 2.12 -- torch 2.8 unread there);E4B_ABSMAX_DQ=0(orenable_fast_train(..., absmax_dq=False)) turns it off,=1requires it, and expert offload /TRAIN_ARENAkeep the fp32 absmax. In code,compress_expert_absmax_(model)(returns the stacks compressed; assert it). Each wrapped NF4 stack keeps one fp32 absmax per 64 weights — about 2.8 GB on Mixtral-8x7B and 1.8 GB on Qwen3-30B-A3B; this stores it the way bitsandbytes'quantize_4bit(compress_statistics=True)does (the offset, 8-bit codes, one fp32 per 256), 3.94x smaller. The absmax values become bitsandbytes' double-quantized ones, which is what Unsloth's double-quant trains on, and they are lossy against fp32. With the switch off nothing changes.enable_fast_trainand the reference loop expand one layer's absmax to fp32 just in time (expert_absmax_fp32). Expert offload,enable_batched_train, the residency, NVMe and hybrid engines, and the gpt-oss / DeepSeek-V4 per-expert forwards refuse a compressed model withAbsmaxCompressedError. No speed or memory claim is registered for it yet. - Environment: Linux, NVIDIA CUDA, torch>=2.2, bitsandbytes>=0.43, transformers>=5.0; CI tests Python 3.11.
[fast]installs grouped-nf4-gemm at thefastextra's floor inpyproject.toml(grouped-nf4-gemm >= 0.43.0 at this commit; validated by CI), which needs Triton (Linux-only) on an sm_80-or-newer GPU.
Limitations
enable_fast_trainreturns0rather than raising whennf4_qlorais missing;dgrad=Trueon a kernel cut older than grouped-nf4-gemm 0.7.0 is turned off with aRuntimeWarning. Assert the count.train.main()does not enable the fast path for you (../../CONTRIBUTING.md).- The fused path changes the expert summation order (opt-in on purpose) and its model-level accuracy is measured, not assumed (
engines/fast.pydocstring). enable_batched_trainwins only a toy microbench; at Qwen3-30B width it gives no speed-up and the highest peak memory (engines/batched.py,e4b.train.fast-train-dgrad) — width-dependent: on tp1 it is the faster of the two paths measured at Granite-3B width (e4b.train.parity.tp1.granite.batched.2026-09-05). It andenable_fast_trainare mutually exclusive on a module.- What to check on a batched arm — engagement, not the count.
enable_batched_trainfalls back to the reference forward per call whenever the padded gather exceeds_PAD_WASTE_LIMIT(engines/batched.py), so its return value stays positive while some layers run the per-expert loop on some steps. In the code tp1 measured (0.35.0) that fallback was silent and uncounted, and three batched arms are VOID on exactly this — OLMoE, Qwen3 and Gemma-4 (e4b.train.parity.tp1.olmoe.batched.2026-09-05,…qwen3.batched…,…gemma4.batched…; on 9 of Gemma-4's 60 steps no layer reached the kernel) — while Mixtral's 8-expert and Granite's 40-expert shapes engaged everywhere. As of 0.35.1 the fallback is counted: readbatched_fallback_stats(model)after a step (#402; those VOID rows are why it exists) and require at least two kernel calls per patched layer per step; a VOID row carries no parity number, however its loss curve reads. - A wrapper whose base breaks the stock-epilogue contract is refused, loudly.
enable_fast,enable_fast_trainandenable_batched_trainraiseEpilogueContractError(0.35.1, #402) rather than computeact(gate) * upover a base that applies biases, clamps or another gating rule; DeepSeek-V4 supplies its epilogue through_apply_gateand is admitted, gpt-oss is the family the refusal protects. - What to check on gpt-oss — the count is zero, by design. The loader builds its experts bare (no
ExpertsLoRA), soenable_fast_trainandenable_batched_trainboth return0— REFUSED rows on tp1 (e4b.train.parity.tp1.gptoss.fused.2026-09-05,.batched). Attention-only QLoRA over the frozen experts trains (e4b.train.parity.tp1.gptoss.attn_only.2026-09-05). Its experts train only on two unlicensed routes:ExpertsLoRA's unfused reference loop behindE4B_GPTOSS_EXPERT_LORA=1(graded on CPU against a transformers-matching oracle, unmeasured on hardware; the fused and batched enablers refuse it), and the kernel package's experimentalExpertsMxfp4LoRA(mxfp4-moe-training-and-residency.md). - Non-checkpointed offload training is unsupported and fails loudly; the trainer always enables gradient checkpointing.
- gpt-oss:
ExpertsLoRAapplies its biases and clamped GLU (_apply_gate) on the unfused reference loop only, opt-in throughE4B_GPTOSS_EXPERT_LORA=1; no fused path applies the biases. - The registered training speed-up claim is retired: restated against a current baseline, roughly half of it is upstream's own fused loop (
e4b.retired.13.47x-training-speedup, retired;../STATUS.md, "What changed"). - The Unsloth head-to-head is one workload per box, and its 200-step curve favours Unsloth. The 60-step position (e4b faster per step, lower peak VRAM and energy, comparable held-out loss) is never quoted without the 200-step row (
e4b.train.h2h.unsloth.qwen3.5090.2026-09-05.curve-n200: Unsloth's held-out loss lower at the end of the curve). ≈86 tokens per step at batch 1 on a resident 30B MoE; a larger-batch, longer-sequence workload is the separate tp4 field-recipe position (below); the candidate causes of the separation are not established. The other families are now measured on one box (tp2/P40, 2026-09-06, above) and are statuses, not one number: two positions (Qwen3 1.457, e4b faster; Mixtral 0.361, Unsloth faster resident at 29.163 GB against e4b's expert-offload 3.223 GB — the designs are named wherever that ratio is quoted), a VOID, a HARNESS_ERROR, and refusals on both sides for gpt-oss; no cross-family ratio and nothing licensed. - Attention q/k/v/o by STRUCTURE, not family name.
add_attention_loraandquantize_attention_projections_4bitsharedetect_attention_projections:q_proj/k_proj/o_projmust be supported linears;v_projmay be a supported linear or absent/None(Gemma-4attention_k_eq_vlayers). The expected count islen(candidates), never4 * n_layers. A layer lackingk_projis refused; a bias-carrying projection is refused (gpt-oss). This rule fixed the count check that killed tp2/P40's arms (#412), and the arms have run since (P56, tp4, P67). The Gemma-4 attention-4-bit configuration is supported since lane P67 (2026-09-24), under the family's measured floor band rather than tp1's constant band (#713).
Related
bitsandbytes-moe-load-in-4bit-still-ooms.md·run-moe-larger-than-vram.md·offload-moe-experts-to-cpu-or-nvme.md·mxfp4-moe-training-and-residency.md../CHOOSING.md·../METHODOLOGY.md·../STATUS.md·../../README.md
Measured result
Claim e4b.train.flagship-matrix (measured, receipts in this repository): across two 30B-class MoEs — Qwen3-30B-A3B and Gemma-4-26B-A4B — five datasets each, 200 steps per cell, the fused training path runs 1.52–1.81× faster per step at 0.75–0.81× peak VRAM and 0.80–0.92× energy per step, with parity on both registered training-loss criteria: absolute final training-loss delta and median absolute step-wise training-loss delta, each at most 0.05. These are not held-out evaluation criteria. That ratio is fused-vs-reference with the same offload and checkpointing in both arms. A per-step multiple quoted against the per-expert Python loop on a smaller model (the OLMoE per-expert-loop comparison) has a different denominator and different models; it is not this number, and the two do not compose.
The frozen-byte result is a separate piece of evidence. The Qwen3 matrix receipt withdraws its B1 check: it compared empty placeholders, so it did not establish byte preservation in those cells. The independent fused-train gate hashed 16.31 GB, skipped no empty tensors and detected a deliberate byte flip; that result belongs to that run. The second model's C1 protocol explicitly requires the corrected checks. The matrix's speed, memory, energy and training-loss measurements stand; the invalid B1 does not become valid because another run checked the bytes.
Claim family e4b.train.parity.tp1.* (measured, receipts in ../../bench/train-parity-20260905/tp1/; lane tp1, 2026-09-05, the shipped code on one rented RTX 5090, real weights through the direct loader, 60 steps on the registered clinical text, verdicts in the same registered units with every row classified OK / REFUSED / HARNESS_ERROR / ALARM / OOM / NOT_RUN / EXPERIMENTAL, cost reported and never gated): enable_fast_train(dgrad=True) PASSES on OLMoE-1B-7B-Instruct (e4b.train.parity.tp1.olmoe.fused.2026-09-05, the first fused-vs-reference reading on a registered text with real weights for that family), on Qwen3-30B-A3B resident on the 32 GB card (…qwen3.fused…), on Gemma-4-26B-A4B-it — the -it checkpoint, loaded without #344 on that host — with the step-wise median inside the band by a small margin (…gemma4.fused…), and on Mixtral-8x7B-Instruct under offload=True at half the reference loop's peak VRAM (…mixtral.fused…; the family entered model_families on it); and on Granite-3.1-3B-A800M — the family's first direct real-weight load — on the corrected-counter re-run (…granite.fused…; the first attempt is a kept HARNESS_ERROR row, …granite.fused.attempt1…, a closure bug in the harness's kernel counter, not the shipped code; the family entered model_families on the re-run); enable_batched_train PASSES on Granite (…granite.batched…) and Mixtral (…mixtral.batched…) and is VOID on OLMoE, Qwen3 and Gemma-4 — the kernel was not reached on every layer (below); gpt-oss's fused and batched arms are REFUSED (below). A PASS there is a PASS on one text; no convergence claim and no cross-family ratio is made.
Claim family e4b.train.h2h.unsloth.<family>.5090.2026-09-06 (measured, receipts in ../../bench/h2h-20260906/tp2/; lane tp2 / P40, 2026-09-06, one rented RTX 5090, the pre-registration verbatim in the bundle): the head-to-head against Unsloth, per family — P38's fixture exactly on all six families, one box, one session, e4b at the shipped cut a user installs today (0.35.1 + grouped-nf4-gemm 0.30.2 from PyPI, NF4 attention via TRAIN_ATTN_4BIT) against Unsloth 2026.9.2; every attempt a row, a position only where both primary arms are VALID, the quality reading beside it. Positions on two families: Qwen3-30B-A3B, ratio of median s/step Unsloth/e4b 1.457 (5.986 vs 4.108 s — e4b faster per step; peak 21.371 vs 23.141 GB; 383.9 vs 561.4 J/step; held-out COMPARABLE, Δ +0.0152 ≤ 0.05) — +3.1% from P38's 1.413, inside the pre-registered ±10% anchor band: the sign and size of the per-step position reproduce on a second box (…qwen3.5090.2026-09-06, ….quality-n60, ….e4b-internal-parity); and Mixtral-8x7B, where the footprint trade leads the row: the e4b arm trained its experts under CPU offload at a 3.223 GB peak (the registered design for this family, tp1's offload=True; a trainable-on-smaller-cards capability, its own row …mixtral.5090.2026-09-06.footprint) while Unsloth ran resident (its only mode) at 29.163 GB, and what that VRAM buys it is speed per step — ratio 0.361, a footprint-vs-speed trade, not a kernel deficit; e4b's energy is lower (298.3 vs 350.2 J/step); held-out COMPARABLE, Δ −0.0087; Unsloth's predicted resident OOM did not happen (…mixtral.5090.2026-09-06, ….quality-n60, ….e4b-internal-parity). Statuses and coverage rows are the result on the other four: Granite — the comparator could not train the experts: Unsloth's arm completed but is VOID (it attached LoRA to the attention only, 2,621,440 trainable against e4b's 49,807,360 with ExpertsLoRA on all 32 MoE layers; its MoE-LoRA path never engaged on granitemoe; …granite.5090.2026-09-06.coverage, ….arm.unsloth.ckpt_unsloth), e4b's arms VALID (fused ×5.59 its reference); OLMoE — the comparator could not train the family at all: Unsloth's process died at MoE-LoRA engage before its first receipt write (HARNESS_ERROR; …olmoe.5090.2026-09-06.coverage, ….arm.unsloth.ckpt_unsloth), e4b's arms VALID (fused ×3.73); gpt-oss — three refusals as pre-registered (e4b attention-4-bit refuses on 96/96 bias-carrying projections, the fused path patches nothing, Unsloth's MXFP4 load conversion fails; …gptoss….arm.*); Gemma-4 — both e4b attention-4-bit arms died on the harness's projection-count check, 100 converted where it expected 4 · n_layers = 120 (#412, fixed since by #435; run since in P56 and tp4 and not licensed, #713; the bf16-attention fast_train path stays as tp1 left it) while Unsloth's arm is OK · VALID at 3.510 s/step (…gemma4….arm.unsloth.ckpt_unsloth). e4b's internal fused-vs-reference parity PASSES on all four families that ran both arms (×5.59 / ×3.73 / ×2.69 / ×1.23 per step; informational, tp1 owns the licence). Nothing supersedes P38 — two boxes, two measurements, never averaged — and P38's 200-step curve row still stands beside any Qwen3 position.
Claim family e4b.train.h2h.unsloth.qwen3.5090.2026-09-05 (measured, receipts in ../../bench/h2h-20260905/p38/; lane p38, 2026-09-05, one rented RTX 5090, the pre-registration verbatim in the bundle): the head-to-head against Unsloth, end-to-end, on one identical training problem — Qwen3-30B-A3B at one pinned revision, the registered clinical fixture tokenised once and asserted by sha in every arm, seq 512, r 8 / α 16 on attention q/k/v/o and every expert, the router frozen, the same optimizer, LR, batch, steps, precision and held-out eval on both sides; this package's fused dgrad path with NF4 attention (enable_fast_train(dgrad=True) + the shipped TRAIN_ATTN_4BIT mechanism) against Unsloth 2026.9.2's 4-bit MoE QLoRA path. At 60 steps e4b is faster per step (ratio of median s/step, Unsloth/e4b 1.413: 2.151 vs 1.522 s), lower in peak VRAM (21.371 vs 23.141 GB) and energy per step (157.1 vs 224.7 J), reaches a held-out loss of 0.32 sooner (92.5 vs 130.3 s), and the two are comparable on held-out loss (0.2923 vs 0.2975, |Δ| 0.0052 ≤ the 0.05 reading threshold; …quality-n60). At 200 steps the curves separate in Unsloth's favour (0.2713 vs 0.2881, …curve-n200) — e4b's flattens near 0.29 from step 60 while Unsloth's keeps falling; that row is quoted beside the position wherever the position is quoted, and its causes (the eval schedule, the checkpointing mode, the two stacks' transformers/peft versions, the expert adapter's precision — bf16 here because the loader passes the model dtype to ExpertsLoRA, fp32 on Unsloth's side) are candidates, not established. One workload (≈86 tokens per step, batch 1, resident), one box, one family; no general speed claim; nothing licensed (…e4b-internal-parity is the informational fused-vs-reference PASS on that box, tp1 owns the licence).
Claim e4b.train.h2h.unsloth.qwen3.5090.2026-10-02 (measured, receipts in ../../bench/h2h-2026-10-02/tc1/; lane TC1, 2026-10-02, three rented RTX 5090 hosts, the pre-registration with its amendments in bench/tc1/): the field-recipe position at matched work — Qwen3-30B-A3B at the Unsloth notebooks' recipe with the same 642,514,944 parameters, one per-slot LoRA init and fp32 adapters in both frameworks, Unsloth 2026.9.14 on torch 2.12.1+cu130 with its grouped_mm backend engaged — reads Unsloth/e4b 1.437 [1.434, 1.440] (5.688 vs 8.171 s/step over two draws each) with held-out EQUIVALENT at N = 20 and over 200 steps (.curve-n200), the parity control PASSING, and Unsloth's peak VRAM 3.57 GB lower and energy ×0.72 at that configuration. It supersedes the 2026-09-19 row (4.490), whose Unsloth arm ran on torch 2.8 and therefore on Unsloth's per-expert loop (.loop-fallback-t28: still 6.565 × on a torch-2.8 install). Both steps are host-launch-bound on this card; absolute s/step does not travel between hosts, the within-box ratio does (.host-variance). On an H100 NVL the sign reverses (e4b.train.h2h.unsloth.qwen3.h100.2026-10-02, receipts in ../../bench/h2h-2026-10-02/tc1c/): Unsloth/e4b 0.621 [0.615, 0.628] (medians of two draws) — Unsloth faster by 1.61 × at lower VRAM and energy with the same loss — because the grouped GEMM its path routes through is one launch per call on Hopper and not on Blackwell (.dispatch-profile); the 1.437 is a 5090 position and no "e4b faster" position is quoted for the H100 class.
Evidence
Register: ../claims.json; all of these are measured with receipts in this repository.
e4b.train.olmoe-converges— QLoRA on frozen NF4 experts improves a held-out eval on OLMoE-1B-7B.e4b.train.ablation— adapter placement: experts+attention is the best eval, attention-only the efficiency pick; training the router hurts.e4b.train.fast-train-dgrad—enable_fast_train(dgrad=True)is the fastest lane at real width; the batched path wins only the microbench.e4b.train.flagship-matrix— two 30B-class MoEs by five datasets: fused path faster per step at lower VRAM and energy, with parity on the registered training-loss criteria. Frozen-byte evidence comes separately from the fused-train gate; the Qwen3 matrix's original B1 is withdrawn.e4b.offload.fits-30b-class— with expert offload, Qwen3-30B-A3B and Gemma-4-26B-A4B QLoRA-train on 12 GB.e4b.train.parity.tp1.olmoe.fused.2026-09-05,e4b.train.parity.tp1.qwen3.fused.2026-09-05,e4b.train.parity.tp1.gemma4.fused.2026-09-05,e4b.train.parity.tp1.mixtral.fused.2026-09-05,e4b.train.parity.tp1.granite.fused.2026-09-05,e4b.train.parity.tp1.granite.batched.2026-09-05,e4b.train.parity.tp1.mixtral.batched.2026-09-05— tp1 (real weights, the shipped code, one RTX 5090): the fused path passes on every family that has one and the batched path on Granite and Mixtral, in the registered units;e4b.train.parity.tp1.<family>.matrix.2026-09-05per family (load + verify + every arm's row, including the VOID and the refusals); every row of the lane is measured.e4b.train.h2h.unsloth.qwen3.5090.2026-09-05— the head-to-head against Unsloth's 4-bit MoE QLoRA path, end-to-end, on one identical training problem (one rented RTX 5090): the s/step ratio, peak VRAM, energy and time-to-target at 60 steps;e4b.train.h2h.unsloth.qwen3.5090.2026-09-05.quality-n60— held-out loss comparable at 60 steps;e4b.train.h2h.unsloth.qwen3.5090.2026-09-05.curve-n200— Unsloth's held-out loss lower at 200 steps, quoted beside the position;e4b.train.h2h.unsloth.qwen3.5090.2026-09-05.e4b-internal-parity— the fused-vs-reference PASS on that box (informational); one row per arm undere4b.train.h2h.unsloth.qwen3.5090.2026-09-05.arm.*.e4b.train.h2h.unsloth.<family>.5090.2026-09-06— tp2/P40, the head-to-head per family (all six, one rented RTX 5090, P38's fixture): positions, each a ratio of median s/step, on Qwen3 (e4b.train.h2h.unsloth.qwen3.5090.2026-09-06, 1.457 — the cross-lane anchor, +3.1% from P38's 1.413) and Mixtral (e4b.train.h2h.unsloth.mixtral.5090.2026-09-06, 0.361 — led by the footprint trade: Unsloth resident vs e4b expert-offload), each with a.quality-n60COMPARABLE row;e4b.train.h2h.unsloth.mixtral.5090.2026-09-06.footprint— the 3.223 GB offload peak against the 29.163 GB resident peak at comparable quality, the trainable-on-smaller-cards result;.coveragerows on Granite and OLMoE (the comparator could not train the experts there — attention-only LoRA / a crash at MoE-LoRA engage — with its own log lines quoted);.e4b-internal-parityPASS rows on Granite, OLMoE, Qwen3 and Mixtral; and one row per attempt undere4b.train.h2h.unsloth.<family>.5090.2026-09-06.arm.*, including Granite's Unsloth VOID, OLMoE's Unsloth HARNESS_ERROR, gpt-oss's three REFUSED rows and Gemma-4's twovoid_attn4rows (#412).e4b.train.h2h.unsloth.qwen3.5090.2026-10-03— TC1 amendment 19, the matched position after amendments 10-15: Unsloth/e4b 1.997 [1.980, 2.014] (medians of two draws), matched set inside the draw noise;e4b.train.h2h.axolotl.qwen3.5090.2026-10-03axolotl/e4b 2.775 (1.979 on another host).e4b.train.h2h.unsloth.qwen3.h100.2026-10-04— TC1c amendment 1, the same matched set on an H100 NVL after amendments 10-15: Unsloth/e4b 0.817 [0.799, 0.836] (medians of two draws), Unsloth still faster per step there (1.22x, was 1.61x), matched set EQUIVALENT.e4b.train.h2h.unsloth.qwen3.h100.2026-10-04.moe-keep— TC1c amendment 2, a labelled row: with e4b keeping all 48 layers' MoE activations (opt-in) Unsloth/e4b 1.100 [1.088, 1.111] (medians of two draws), e4b faster per step on the H100 at 9.8 GB more peak VRAM.e4b.train.h2h.unsloth.qwen3.h100.2026-10-04.route/.moe-keep-route— TC1c amendment 4, labelled: grouped-nf4-gemm's opt-in grouped_mm route as shipped made e4b slower (0.664 / 0.934, ratios of median s/step) because its dequant kernel ran ~4x slower than bitsandbytes'..route-v2/.moe-keep-route-v2— TC1c amendment 6, labelled: with #452's dequant (bit-equal, ~8.5x faster) the route makes e4b faster than Unsloth on the H100, 1.030 / 1.325, matched set EQUIVALENT; it qualifies as grouped-nf4-gemm's sm_90 default (grouped-nf4-gemm#454).e4b.train.host-reuse.qwen3.5090.2026-10-04— TC1 amendment 20, grouped-nf4-gemm's per-pass host reuse (values identical) at 0.933 (shipped) / 0.951 (matched) of before, in median s/step; on by default.e4b.train.moe-keep.qwen3.5090.2026-10-04— TC1 amendment 21, keeping the last n layers' MoE activations instead of recomputing them (E4B_MOE_KEEP_LAYERS + the compact delta, gradients identical): 0.835 of the median s/step with 32 of 48 kept (+4.5 GB), 0.926 with 16 (+2.3 GB); opt-in, recommended where the headroom exists.e4b.train.h2h.unsloth.qwen3.5090.2026-10-02— TC1, the field-recipe position against Unsloth on Qwen3-30B-A3B at matched work with Unsloth's grouped path engaged (1.437 [1.434, 1.440], medians of two draws, e4b faster per step; peak VRAM and energy in Unsloth's favour), with its.quality-n20EQUIVALENT,.e4b-internal-parityPASS,.secondary-mb1,.curve-n200(EQUIVALENT at every eval to 200 steps),.plateau-shipped-n200,.loop-fallback-t28,.native-best,.shipped-vs-matched,.e4b-t212,.hf-oom,.host-variance,.anchor-p38-fixtureand.scaling-pointsrows;e4b.train.h2h.unsloth.qwen3.5090.2026-09-19(4.490) is superseded by it: that row measured Unsloth's torch-2.8 per-expert loop.e4b.train.h2h.axolotl.qwen3.5090.2026-10-02— axolotl 0.20.0 at matched work on the same card class: axolotl/e4b 1.416 [1.398, 1.435] (medians of two draws; e4b faster per step; axolotl lower peak, ×2.31 energy), re-run after the harness defect behind the retired.axolotl-unsupportedrow was fixed;.scattermoe-native— axolotl's scattermoe native-best at 0.929 × e4b's matched step, one draw, a labelled row; against e4b as shipped on one box its draws were 10.8 % apart, so no ratio is quoted (TC1 amendment 8 re-asks it over 200 steps)..native-vs-native— each framework as its users run it, on one box: Unsloth native-best / e4b as shipped 1.794 [1.790, 1.797], e4b faster per step; a labelled row, and no "fastest" statement (P13 UNTESTED on its axolotl half).e4b.train.h2h.axolotl.qwen3.5090.2026-10-02.native-steady-state— over steps 101..200 of a 200-step run axolotl's scattermoe native-best steps at 0.911 [0.902, 0.921] × e4b as shipped (medians of two draws), and 0.901 [0.892, 0.910] on a second host, axolotl faster at steady state on both; e4b still finishes a 200-step run first (axolotl's warm-up), and axolotl reaches the matched held-out curve where e4b as shipped sits 0.024-0.027 above it.e4b.train.host-syncs.qwen3.5090.2026-10-03— e4b against itself: 13 host syncs per fused MoE layer pass cut to 1 (#946 + grouped-nf4-gemm's pinned ring) step the field recipe at 0.866 (shipped) / 0.847 (matched) of the legacy path's median s/step on one 5090, loss unchanged; the cross-framework rows predate it.e4b.train.lora-delta-lean.qwen3.5090.2026-10-03— and after it, grouped-nf4-gemm's exact trim of the padded LoRA delta (#440) steps it at 0.939 (shipped) / 0.911 (matched) of the previous body's median s/step, loss unchanged.e4b.train.prefill-tile-rule.qwen3.5090.2026-10-03— then the grouped GEMM's M-tile sized from the actual group sizes (grouped-nf4-gemm#441) steps it at 0.924 (shipped) / 0.968 (matched) of the largest-group rule, outputs identical.e4b.train.fused-rmsnorm.qwen3.5090.2026-10-03— then the frozen RMSNorms fused into one launch each way (#961, near-exact) step it at 0.924 (shipped) / 0.959 (matched) of the composite, held-out within 0.0021.e4b.train.h2h.axolotl.qwen3.5090.2026-10-03.native-steady-state— with all of that, axolotl's scattermoe / e4b as shipped over steps 101..200 is 1.238 [1.231, 1.246] (medians of two draws) on one 5090 host and 1.146 [1.129, 1.164] on a second: e4b now faster at steady state (the 2026-10-02 row stands for the code before #945).e4b.train.h2h.unsloth.qwen3.h100.2026-10-02— TC1c, the same matched set on an H100 NVL, e4b before #945: Unsloth/e4b 0.621 [0.615, 0.628] (medians of two draws; Unsloth faster, lower VRAM and energy, EQUIVALENT loss), with its.quality-n20,.e4b-internal-parityand.dispatch-profilerows — the 5090 position is card-specific.e4b.train.h2h.unsloth.coverage.5090.2026-09-19— what tp4 could not quote on the 2026-09-19 cut (Granite / Qwen3.6 attention-only, OLMoE's arm died) stands until lane TC2 re-asks it.e4b.train.h2h.unsloth.qwen3.h100.release-0.45.0— TC1c amendment 8, the H100 position at default settings on 0.45.0 / grouped-nf4-gemm 0.37.0: Unsloth/e4b 1.061 [1.047, 1.075], e4b faster per step, matched set EQUIVALENT; it supersedes 0.817 as the default-settings row.e4b.train.frontier.qwen3.4090-24gb.2026-10-02— TC3, the memory frontier at TC1's matched set: on a 24 GB RTX 4090 e4b trains Qwen3-30B-A3B under expert offload at 11.88 GB peak and 10.52 s/step with the trajectory EQUIVALENT-TO-RESIDENT (.offload-keeps-trajectory), resident e4b OOMs (.e4b-resident-oom), Unsloth trains the set resident at 24.22 GB (.unsloth-resident— the lane's prediction refuted), HF and axolotl neither fit nor offload (.hf-axolotl);e4b.train.frontier.qwen3.a2000-12gb.2026-10-02— the same set on the owned 12 GB RTX A2000 under offload at micro-batch 1 (10.46 GB peak), a hand run labelled so. A fit table, one draw per row — not a position.e4b.train.h2h.hf.granite.5090.2026-10-02/e4b.train.h2h.axolotl.granite.5090.2026-10-02— TC2, Granite-3.1-3B-A800M at matched work, as ratios of median s/step: HF (bf16 experts) 0.971 [0.965, 0.978], faster per step than e4b's fused 4-bit experts at half HF's VRAM (a footprint position), and axolotl's 4-bit path 0.903 (its experts NF4, corrected 2026-10-02);e4b.train.h2h.unsloth.granite.5090.2026-10-02.coverage— Unsloth adapts no expert parameter on either target list.e4b.train.h2h.unsloth.olmoe.5090.2026-10-02— OLMoE-1B-7B, median s/step Unsloth/e4b 1.201 [1.199, 1.204] with its grouped path engaged (e4b faster per step; Unsloth lower VRAM and energy, the same loss), besidee4b.train.h2h.hf.olmoe.5090.2026-10-02(1.255 [1.253, 1.258]);e4b.train.h2h.unsloth.gptoss.5090.2026-10-02.no-common-set— gpt-oss-20b has no common adapter set, an e4b limitation.e4b.train.h2h.unsloth.qwen3_5.5090.2026-10-02— Qwen3.6-35B-A3B at matched work on 32 GB: Unsloth (the family's own expert names) trains the 926 M-parameter set resident at 30.47 GB, e4b's fused path OOMs at both micro-batches and in its reference loop; a resident-only e4b loss, no ratio;.e4b-offload— under expert offload e4b trains the set at 19.35 GB (the frameworks quantise different module sets on this family, so no same-base pair exists);.shipped-labelled— e4b as shipped fits at 32.48 GB and 5.09 s/step, a labelled row.e4b.train.footprint.unsloth.mixtral.5090.2026-10-02— Mixtral-8x7B re-measured with the counters fixed: Unsloth resident 3.74 s/step at 29.1 GB, e4b under offload 19.8–21.2 s/step at 7.16 GB (a footprint row: e4b ×4.07 lower peak, Unsloth about 5.3× faster per step; no position, e4b's offload draws unstable); the two retired Mixtral rows were the old counters.e4b.train.h2h.unsloth.mixtral.5090.2026-10-04— TC2 amendment 6, both resident: e4b now fits Mixtral on 32 GB (31.07 GB), and Unsloth/e4b is 0.697 [0.679, 0.714] on the fused kernels, Unsloth faster at 1.94 GB less peak; superseded by.dense-default;e4b.train.h2h.unsloth.qwen3_5.5090.2026-10-04— Qwen3.6 re-asked resident: e4b still OOMs at both micro-batches (now at step 2).e4b.train.h2h.hf.granite.5090.2026-10-04/e4b.train.h2h.axolotl.granite.5090.2026-10-04/e4b.train.h2h.unsloth.olmoe.5090.2026-10-04— TC2 amendment 7 on the current code: e4b faster than each (ratios of median s/step 1.299 / 1.150 / 1.821), superseding 2026-10-02's 0.971 / 0.903 / 1.201;.mixtral…absmax-dq(labelled) — E4B_ABSMAX_DQ brings e4b's Mixtral peak to 29.03 GB, level with Unsloth's;.qwen3_5…fit-mb1-dq-frozen4(labelled) — Qwen3.6 trains resident at micro-batch 1 on the comparator's bytes.e4b.train.h2h.unsloth.mixtral.5090.2026-10-04.dense-default— TC2 amendment 8, e4b at default settings (grouped-nf4-gemm's dense route): Unsloth/e4b 0.836 [0.828, 0.844] (medians of two draws), Unsloth faster, the family's position;e4b.train.h2h.unsloth.qwen3_5.5090.2026-10-04.mb1-dq-frozen4(labelled) — Qwen3.6's first quoted position, Unsloth/e4b 2.049 [2.028, 2.070] (medians of two draws) at micro-batch 1 on the comparator's bytes, e4b faster.
Common wrong approaches
- Applying PEFT's LoRA to the model and reading a loss curve that only trains attention: PEFT never sees the fused experts.
- Calling
enable_fast_trainwithout asserting its return:0means the step is still on the per-expert loop.
Source and freshness
This page is a rendering of docs/solutions/qlora-fused-moe-experts.md at commit 605d21e67958 (sha256 7f877bad422d87da…). 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.
rendered from experts4bit-qlora 0.58.1 · source: pjordanandrsn/experts4bit-qlora@605d21e67958 · checked 2026-10-25