Repository navigation
feat(moe): serve experts of several formats through per-format slot pools - #700
Conversation
|
The direction looks right to me. Taking the union of each format's banks, with zero-byte placeholders on top of the #689 slot pools, keeps the cache changes small, and the per-layer Tested on H100, torch 2.9.0+cu128: the 5 new tests pass. The full Should be fixed or guarded before merge1. Loading a raw checkpoint and
The new test passes only because it uses a hand-written 2. The parallel-to-serial fallback leaks pinned banks. The fallback in 3. Unified-memory GPUs refuse mixed models with default flags. On GB10 or Jetson, auto picks Performance (a follow-up is fine, but worth stating in the PR)4. Shared prefill staging and zero-byte banks.
Options: a dedicated staging region outside the pools for mixed models (costs 2 x E x the widest row per bank), or a staging-aware slot split that fails with the minimum cache size when it cannot fit. 5. A small bf16 pool caps the decode batch of the whole server. Pools start from Nits
|
|
Thanks for the careful review. b218e5b addresses the loader issues and most nits; the pool layout items are left to a follow-up pool policy, and the PR description now lists them under Not covered. 1. Readers. Fixed. 2. Fallback leak. Fixed. 3. Unified-memory GPUs. Not in this PR. Resident experts of several formats stay refused, now with the clear error on the 4, 5. Staging and small pools. Agreed on both. The cache already lays the pools out in whatever order the planner hands it, so these belong to a pool policy (pool order, a minimum slot count per pool that leaves room for staging and the decode batch, filling small pools when they fit) in a follow-up PR. Nits.
|
b218e5b to
5ca03f0
Compare
Upstream 0bca88f..11c1adb: GGUF on vendored ggml (FlashML-org#690 and the per-arch loaders), MoE slot pools (FlashML-org#689, FlashML-org#700), GGUF experts on the cpu/hybrid executor (FlashML-org#704), the NVFP4 MoE prefill GEMM rewrite (FlashML-org#723), triton decode split-k by context length (FlashML-org#729), graph upload at capture (FlashML-org#697), transformers 5.18. Resolution: - GGUF: upstream's stack replaces ours (dense llama/qwen2/qwen3, qwen3moe, qwen35moe loaders, q4_0/gguf expert banks, our ggml kernels and their tests). Re-ported on top: rope-only --hf-overrides (YaRN) for the llama/qwen2/qwen3/qwen3moe GGUF parsers, and the scalar head_count_kv of Gemma-4 GGUFs (upstream PR FlashML-org#190). Gemma-4 E2B/E4B GGUF is not re-ported. - offload cache: upstream's pooled cache, with our disk tier (one pool only), fill_slots preload per pool, sync-free prefill invalidation for a contiguous staging range, hit-D2D-by-default wording and the HIP graph per-bank copy. - NVFP4 prefill: upstream's GEMM and per-GPU tuning supersede our sm_89 tile config and JOIN_B. - triton decode attention: upstream's split-k over our fp8/nvfp4 KV paths and HIP head padding; DFlash verify graphs are uploaded at capture too. - embedding: upstream's embedding quant method, with --embed-device cpu in front and the tied head's kernel choice kept for unquantized tables. - engine: DFlash, --kv-reserve-share, shrink-first rebuild, startup hybrid split and the WSL pin probe combined with the slot pools. GPU-checked on the RTX 4090: full suite green; Qwen3.6-35B-A3B NVFP4 prefill ~13.2k / decode ~131 tok/s (baseline 13.6k / 131), Qwen3-30B-A3B Q4_K_M GGUF 14.6k / 259 (was 8.3k / 225). Assisted-by: Claude Opus 5.5
…r RTX), PR FlashML-org#729 (split decode attention), PR FlashML-org#726, PR FlashML-org#700 and GGUF updates
Part of #671. Base for the GGUF experts on the CPU executor, which follows in a separate PR.
What this does
Until now every MoE layer of a model had to use the same expert format;
shared_offload_methodrefused a model whose layers disagree. With this PR each MoE layer keeps its own expert format and its own banks. Nothing in the mechanism is specific to a pair of formats: the layers of one checkpoint can use any of bf16, block FP8, NVFP4 and MXFP4, for example bf16 routed experts in the first and last layers and NVFP4 in the rest. The tests cover bf16 + NVFP4.The first commit is @andy-yang-1's change. The second commit hands the CPU executor every layer's format together with its options and treats all formats alike; the GGUF follow-up needs the options.
How layers with different banks live together
A bank is one kind of per-expert array. Each format has its own set:
bf16gate_up,down(bf16 weights)fp8_blockgate_up,down(FP8 weights),gate_up_scale,down_scale(bf16, one per 128x128 block)nvfp4gate_up,down(packed 4-bit weights),gate_up_scale,down_scale(FP8, one per 16 values),gate_up_global,down_global(fp16, one per row)mxfp4_triton(gpt-oss)gate_up,down(4-bit blocks),gate_up_scale,down_scale(e8m0),gate_up_bias,down_bias(bf16)ds_fp4gate_up,down(packed 4-bit weights),gate_up_scale,down_scale(e8m0, one per 32 values)Host banks. The model's bank set is the union of its formats' sets. A layer whose format has no such bank gets a zero-byte
[E, 0]placeholder there. Each format loads only its own layers, then the parts are merged (merge_expert_banks), andExpertBanks.layer_formatsrecords which format each layer uses.GPU slot cache. Each bank is one GPU buffer, as before. The slot pools of feat(moe): split the offload expert cache into slot pools #689 cut every buffer into one region per pool, and here the layers are grouped by format, so each format gets its own pool with its own LRU and its own row size per bank. The same bank name can hold a different dtype and row shape in each pool (
gate_up_scaleis bf16 blocks forfp8_blockand FP8 fornvfp4), and a pool whose format has no such bank takes zero bytes in it. With bf16 and NVFP4 layers, for example:Each layer's kernel reads the views of its own pool. Staging, the fused copy plan and the hit-D2D batch skip the zero-byte banks.
FTW. The converter writes each layer's own banks and records
layer_quant_formats; the reader fills the missing banks with placeholders.CPU executor (hybrid decode). One format object per distinct format; each layer points to one of them. All formats must share H and I. See the next section for how the formats reach the executor.
What cannot be mixed:
nvfp4_marlin,nvfp4_b12x): refused when loading the banks.--moe-strategy cpu,--moe-cpu-layersand resident experts: refused at startup.How GGUF fits
GGUF already has a different quant per layer: in Qwen3.8-Flash-Next GSQ-RCO IQ3_XXS, one layer is IQ2_XS gate_up with IQ4_NL down, another IQ3_S gate_up with Q2_0 down. That case is simpler: every layer has the same two banks,
gate_upanddown, holding raw ggml rows, and only the row size changes with the type. #690 tags each layer with its (gate_up type, down type), so the same slot pools already give each type pair its own pool. No placeholders are needed.What GGUF still needs is for the CPU executor to know each layer's ggml types, and the format name alone (
gguf) cannot carry that. The second commit makes room for it.Each layer's CPU format and options (second commit)
(name, options):MoEMethod.cpu_spec()returns(cpu_format, kernel.cpu_options(cfg)), and settings that belong to one format travel as its options.ds_fp4now carries its fp8 activation block as{"act_block": ...}instead of a separate executor argument taken from the first layer; the GGUF follow-up will pass("gguf", {"gate_up_type": ..., "down_type": ...}).(name, options), and the C++ constructor takes that list plus each layer's index into it. No format is treated as the main one, andset_layer_formatfrom the first commit is gone.ds_fp4andmxfp4_tritonno longer have to be the only format in the executor.CpuMoeExecutortakes the formats only aslayer_formats; without it every layer usescache.quant_formatwith no options. Callers that build an executor directly (ft bench bw, tests) pass e.g.layer_formats=[("ds_fp4", {"act_block": 128})] * num_layers.fused) path now passes every layer's method when it loads the experts, so experts of several formats stop there with the refusal from the first commit. Before, it built every layer's banks in layer 0's format; with dummy banks that failed later inattach_resident_bankswith a bank-set mismatch.Review fixes (third commit)
layers(bank layer ids) and skips the other layers' tensors before reading them; completeness is checked againstlayers. This covers the generic NVFP4 reader, the generic stacked bf16 reader, and thedeepseek_v4,deepseek_v41,glm5_next,gpt_ossandqwen3_5_moereaders._mixed_expert_bankshands each format its own layers, so each layer is read once.pin_bankstakes per-layer dicts keyed by layer id, so the dummy path labels the layers of one format with their real ids.OffloadMoeCacherefuses any resident bank spec inlayer_layouts, instead of comparing role names.alloc_layer_bankshad no callers left and is removed.Validation
Tested on: NVIDIA H100 80GB HBM3, driver 580.95.05, CUDA 13.0; Intel Xeon Platinum 8480+ (13 physical cores visible to the process); torch 2.14.1+cu130, Python 3.14.3, gcc 11.4. No checkpoint was loaded; the tests below build their own experts.
python setup.py build_ext --inplace pytest tests -m "not slow" --ignore=tests/e2etest_scheme_for_agrees_with_the_stored_tensors[apodex/Apodex-1.1-mini-NVFP4[index-only]], a local HF cache entry with an unsupportedmodelopt_mixedconfig.tests/moe/test_mixed_expert_formats.pybuilds one model with bf16 and NVFP4 layers; its 5 tests run here and pass: the engine binds the most common format and checks every layer's banks; dummy banks merge the roles with placeholders; decode and overlap prefill through the per-format pools match a dequantized reference; the CPU executor computes each layer in its own format, also in hybrid decode; the FTW round trip. A sixth test checks that_mixed_expert_banksgives each format's reader its own layers and resolves every reader before reading any.tests/models/test_qwen3_5_moe_weight.py(generic NVFP4 reader on five checkpoint layouts, theqwen3_5_moeblock-FP8 reader serial and parallel) andtests/models/test_models_loader.py(stacked bf16) also read one layer throughlayers.Not covered
experts.gate_up_proj/experts.down_projform, andqwen3_5_moehands its stacked bf16 experts to it only when the model-wideexpert_quantis"none".fused) experts of several formats: refused at startup. On unified-memory GPUs, whereautopicksfused, such a model needs--moe-strategy offloadorhybrid.max_running_reqfor the whole server; the slot limit is the minimum over all kernels (no kernel with a limit can be mixed today); zero-byte banks still launch empty copy blocks.--moe-hybrid-max-fetch auto) reads theft bench bwentry of the most common format for every layer.