[Bugfix][Perf][CPU] Restore vectorized silu_and_mul on CPU; handle uneven hidden dim#1
Open
jaylisde wants to merge 1 commit into
Open
[Bugfix][Perf][CPU] Restore vectorized silu_and_mul on CPU; handle uneven hidden dim#1jaylisde wants to merge 1 commit into
jaylisde wants to merge 1 commit into
Conversation
…den dim PR vllm-project#22145 (Aug 2025) routed all CPU SiluAndMul calls through PyTorch native to dodge a TORCH_CHECK that fired on Qwen2.5 with non-aligned hidden sizes. On AMD EPYC 9R14 the eager-mode "very slight" drop is 4-8x on prefill. Three fixes: 1. csrc/cpu/activation.cpp -- replace the TORCH_CHECK(d % VEC_ELEM_NUM == 0) with a scalar tail loop that handles the remainder elements. 2. csrc/cpu/cpu_types_x86.hpp -- BF16Vec8 and FP16Vec8 ::save(void*) were doing aligned MOVDQA (*reinterpret_cast<__m128i*>(ptr) = reg). Switch to _mm_storeu_si128. The aligned variant segfaults whenever the per-token write offset (output + i*d) is not 16-byte aligned -- any odd d once i >= 1. 3. vllm/model_executor/layers/activation.py -- with the kernel fixed, restore the C++ dispatch on CPU for SiluAndMul. The kernel template also serves gelu_and_mul, gelu_tanh_and_mul, gelu_new, gelu_fast, gelu_quick. Those were never explicitly Python-fallbacked, so they relied on the CustomOp default (forward_cpu -> forward_native) and any caller that used self.op directly on CPU would have crashed on non-aligned d. This fix makes the kernel correct for them too. Microbench, AMD EPYC 9R14 (Zen 4, AVX-512+BF16, no AMX), 64 threads, bf16: shape native (us) cpp (us) speedup Llama-3-8B prefill M=4096 28387 6068 4.68x Llama-3-70B prefill M=4096 17219 2147 8.02x D=18944 M=4096 38363 4893 7.84x D=18943 M=4096 (uneven) 40378 4974 8.12x D=14329 M=4096 (uneven) 29467 3711 7.94x Llama-3-8B decode M=16 62 49 1.28x Llama-3-70B decode M=16 62 36 1.74x C++ runs the activation in fp32 then casts back; on D=14329 the C++ output is closer to the fp32 reference than forward_native (max abs diff 0.0156 vs 0.0330). Test plan: pytest tests/kernels/core/test_cpu_activation.py # 96 passed, 16 skipped (Arm-only paths) D parametrization is extended with 513 and 2049 to exercise the new scalar-tail path. Signed-off-by: Shaojie Li <jieshao@amazon.com>
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.
Purpose
PR vllm-project#22145 (Aug 2025) routed all CPU
SiluAndMulcalls through PyTorch native (F.silu(x[..., :d]) * x[..., d:]) to dodge aTORCH_CHECKincsrc/cpu/activation.cppthat fired on Qwen2.5 with non-aligned hidden sizes. The PR claimed "for eager mode, the performance drop will be very slight." On AMD EPYC 9R14 the actual eager-mode drop is 4–8× on prefill.This PR fixes the underlying kernel and restores C++ dispatch.
Three changes:
csrc/cpu/activation.cpp— replaceTORCH_CHECK(d % VEC_ELEM_NUM == 0)with a scalar tail loop that handles the remainder elements.csrc/cpu/cpu_types_x86.hpp—BF16Vec8/FP16Vec8save(void*)were doing alignedMOVDQAvia*reinterpret_cast<__m128i*>(ptr) = reg. Switch to_mm_storeu_si128. The aligned variant segfaults whenever the per-token write offset (output + i * d) is not 16-byte aligned — any odddoncei ≥ 1. This was latent onmainbecause theTORCH_CHECKalways tripped first; once [Bugfix][Perf][CPU] Restore vectorized silu_and_mul on CPU; handle uneven hidden dim #1 lets unevendthrough, the unaligned write is exercised. Caught while developing this patch (D=14337, num_tokens=2→ SIGSEGV before thecpu_types_x86.hppchange).vllm/model_executor/layers/activation.py— with the kernel fixed, restore the C++ dispatch on CPU forSiluAndMul.The same kernel template also serves
gelu_and_mul,gelu_tanh_and_mul,gelu_new,gelu_fast, andgelu_quick. Those were never explicitly Python-fallbacked, so they relied on theCustomOpdefault (forward_cpu→forward_native); any caller that usedself.opdirectly on CPU would have hit the sameTORCH_CHECK. This fix makes the kernel correct for them too.Reproducing on
mainSiluAndMulitself dodges this since vllm-project#22145 by going throughforward_native, but the underlying_C.silu_and_mul,_C.gelu_and_mul,_C.gelu_tanh_and_mul,_C.gelu_new,_C.gelu_fast,_C.gelu_quickare all still affected when called directly.Perf regression from the Python fallback (this is the visible one for users):
Test Plan
Dparametrization is extended with 513 and 2049 to exercise the new scalar-tail path. The existing values (512, 2048) only ever covered alignedd.Test Result
Microbench, AMD EPYC 9R14 (Zen 4, AVX-512 + BF16, no AMX), 64 threads, bf16:
C++ runs the activation in fp32 then casts back; on D=14329 the C++ output is closer to the fp32 reference than
forward_native(max abs diff 0.0156 vs 0.0330).Notes
The
_mm_storeu_si128change incpu_types_x86.hppis a one-line correctness fix on a primitive shared by other CPU kernels. On Zen 4 (and Intel SPR),MOVDQUruns at the same throughput asMOVDQAwhen the address happens to be aligned, and onlyMOVDQUis correct when it isn't.AI assistance was used to draft the patch; the benchmarks and tests were run against a locally rebuilt CPU wheel and manually verified.