Repository navigation
perf(cpu-moe): speed up the nvfp4 W4A8 kernels - #851
Merged
Merged
Conversation
The VNNI dots were compute bound per core; with fewer instructions per weight they reach the DRAM read bandwidth. The W4A8 kernel now follows FREETOKEN_CPU_MOE_ISA.
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Nov 2, 2026
FlashML-org#843 (completions n) adopted with fixups after a live check on the 4090; FlashML-org#851 (VNNI nvfp4 CPU kernels) deferred: this VM's CPU exposes no AVX-VNNI or AVX-512; FlashML-org#831 deferred (Windows collection); FlashML-org#832 superseded by FlashML-org#844, which upstream merged. Assisted-by: Claude Opus 5.5
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Nov 4, 2026
Upstream dacac5f..628b88f. Conflict resolutions: - FlashML-org#814 timings: upstream's final version replaces the PR head next adopted (a4a62a5). `timings` now comes from upstream's scheduler-side ReqStats (queue_ms, unrounded spans, draft_n/draft_n_accepted) on the new DetokenizeMsg/UserReply `timings` field; next's old API-side timings_block / gen_timings_block are gone, so there is one source. Option A per user: --enable-timings-report defaults ON on next (BooleanOptionalAction, --no-enable-timings-report turns it off; upstream defaults off). FlashML-org#504 metrics are unchanged: GenTimer + prefill_ms wire field, now on GenDone/GenResult.gen_timings (GenDone/GenResult.timings is upstream's dict); GenTimings drops its unused prompt_ms/predicted_ms. The ReqStats stamps were added to next's own _drain_single_token and _drain_multi_token paths (_drain_verify/MTP came from upstream). DFlash draft counters come from a new ForwardOutput.draft_len (verify_len - 1). Streams carry timings on the last chunk only (upstream). /v1/completions keeps next's n>1 fan-out, disconnect aborts and error codes, and reports prompt 0 / choice 0 timings. Responses passes both metrics= and state=; in streams timings ride the event data, not the response snapshot. - FlashML-org#790, FlashML-org#651: identical to next's earlier adoptions (e920eb4, 56ed267); no code change. For the FlashML-org#651 test, next's pool-pressure variant (371d985) is kept. - FlashML-org#854 attention: next's fp8/nvfp4 KV branches are kept, and every slot load takes upstream's KV_OFFSET_DTYPE cast. - FlashML-org#764 engine: upstream's _disable_unsafe_cpu_moe_graphs sits next to next's DFlash helpers. - FlashML-org#851/FlashML-org#845/FlashML-org#856/FlashML-org#858/FlashML-org#859: no conflicts beyond docs; cli.md merges next's DFlash rows with FlashML-org#856's GGUF/mtp sidecar wording. - core.py: Req gains `stats`. Upstream's output_token_counts is dropped, since next does penalties through token_pool. - tokenizer/server.py, message/*: both next's prefill_ms/logprobs and upstream's timings are kept. Assisted-by: Claude Opus 5.5
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Nov 4, 2026
Upstream merged FlashML-org#651, FlashML-org#790, FlashML-org#814 (adopted here before), and FlashML-org#764, FlashML-org#845, FlashML-org#851 (deferred here); they now come through the sync. FlashML-org#851 was marked closed by the render before upstream/main had its squash commit. Assisted-by: Claude Opus 5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The CPU MoE executor's nvfp4 W4A8 dots were compute bound per core. With weights in L2, main's AVX-512 VNNI kernel does 13.4 GB/s per core, about what each of 15 cores needs to read 189 GB/s, and its AVX-VNNI (256-bit) kernel 9.3 GB/s. This PR keeps the int8 math and rewrites the data path of both kernels with about 3x fewer instructions per weight, so both reach the memory read bandwidth.
Part of #671.
Changes
pshufbthat line up with the activations, with no interleave per row.vpdpbusdoperand as is (no sign trick).-12 * sum(a)per lane is computed with the quant and seeds the accumulator.vcvtph2ps, 16 (AVX-512) or 8 (AVX2) per instruction; the 2^-8 left by the shift is folded into the final scale.lroundloop, with the same int8 values and scales: 4.2-4.7x faster. At 2048x512, top-8, bs = 4 they took about 9% of an executor step.FREETOKEN_CPU_MOE_ISA(main ran the 512-bit kernel at any tier). CPUs without VNNI keep the fp32 W4A16 path unchanged.test_cpu_decode_nvfp4_matches_dequant_then_gpuruns eachFREETOKEN_CPU_MOE_ISAtier (scalar,avx2,avx512; a tier the host lacks is skipped), so an AVX-512 host also checks the 256-bit kernel. It uses I = 752 (47 blocks of 16), so the down dots run their partial-chunk and scalar tails.Results
Xeon Platinum 8559C (Emerald Rapids, 16 cores, KVM guest), 15 CPU MoE threads, RTX PRO 6000 Blackwell Server Edition, driver 595.91.07; main = ee43b2a. Read ceiling (AVX-512 stream, 15 threads): 189 GB/s.
One dot, K = 2048, GB/s of weights + scales, main -> PR: AVX-512 VNNI 13.4 -> 34.4 (1 core, L2) and 169 -> 184 (15 cores, DRAM); AVX-VNNI 9.3 -> 25.5 and 94 -> 180.
ft bench bw --model <checkpoint>, CPU-MoE GB/s, median of 4 runs (AVX2 tier: 3):FREETOKEN_CPU_MOE_ISA=avx2)With the faster CPU side the hybrid backend fetches 19-26% of the misses over PCIe instead of 29-32%.
Hybrid decode, aggregate tok/s of bs concurrent streams, main -> PR, median of 2 server starts x 2 rounds:
--moe-cache-rate, tierEach side benched its own
ft bench bwprofile first. The AVX2 rows run main withFREETOKEN_CPU_MOE_ISA=avx2 FREETOKEN_CPU_MOE_NO_AVX512VNNI=1(its AVX-VNNI kernel) and the PR withFREETOKEN_CPU_MOE_ISA=avx2. Server:ft serve --model <checkpoint> --moe-backend hybrid --moe-cache-rate <r> --max-running-requests 4 --cuda-graph-max-bs 4 --cache-type naive; client: bs streamed/v1/chat/completionsrequests (AIME-25 problems 0..bs-1, thinking on, greedy,ignore_eos, 512 tokens), one warm round, throughput over the window where all streams decode. Spread across server starts is 1-2%. Qwen3.8-Flash-Next gains least: its ~15 ms step is mostly GPU work.Behavior changes
FREETOKEN_CPU_MOE_ISA=avx2on a CPU with AVX-512 VNNI but no AVX-VNNI (Ice Lake-SP, Zen 4) runs the fp32 W4A16 kernel instead of the 512-bit W4A8 one;=scalarruns the scalar fp32 kernel.ft bench bwprofile from main still loads, since the ISA labels are unchanged, but it understates the CPU side. Rerunft bench bwto get the lower fetch fraction.Testing
pytest tests/moe/test_cpu_moe.py tests/moe/test_cpu_moe_fp8_block.py tests/moe/test_mixed_expert_formats.py: 53 passed, 3 skipped (HIP only). The nvfp4 tests also pass withFREETOKEN_CPU_MOE_NO_VNNI=1and withFREETOKEN_CPU_MOE_NO_AVX512VNNI=1. A wrong chunk offset in the 256-bit scalar tail fails the threeavx2cases of the parity test.FREETOKEN_CPU_MOE_ISA=avx2, which the parity test now covers on AVX-512 hosts too.