feat(rocm): V1 sampling ops for Hermes EngineCore - #234
Conversation
TESTERS WANTED — BF16 / FP16 Gemma-4 MoE on ROCmLab proof for this PR was FP8 Gemma-4-26B-A4B MoE on 2× Radeon AI PRO R9700 (gfx1201). The code path here is dtype-agnostic (V1 sampler ops + Hermes
Please test if you have
Minimal matrix
Report back
Related: serve KV fail-fast #227; FP8 MoE prefill/resident package #228 (FP8-proven; MoE structure notes there). Thanks — BF16/FP16 MoE ROCm coverage has to come from the community; FP8 is what we can gate on RDNA4 consumer hardware day-to-day. |
localai-org-maint-bot
left a comment
There was a problem hiding this comment.
The ROCm coverage is valuable, but this adds/registers ten sampling operations without automated correctness coverage. Please add CPU/CUDA-vs-ROCm parity tests for the new sampler surface, including allowed-token masks, temperature/top-k/top-p, min-p, penalties/logit bias, and deterministic sampling seeds. The current documentation-checkpoint and agent-record failures also need to be resolved before merge.
Addressing CHANGES_REQUESTED (localai-org-maint-bot)Automated sampler coverageAdded CPU↔ROCm parity cases in
Lab run on this box (HIP build, dual R9700): CPU CI still exercises the existing hand-oracle sampler suite; ROCm block is opt-in like the CUDA parity block. Docs gates
Merge order (unchanged)Land #234 first → rebase #227 / #228. Happy to add CUDA-style large-vocab ROCm greedy case or penalty/logit-bias matrix next if you want a wider surface before merge. |
a986e81 to
4198bb5
Compare
|
Fixed agent-record: FEATURES.md Gemma4 ROCm cell was 258 chars (limit 220). Shortened; |
60db9a2 to
42ba78e
Compare
Rebase + drop max_tokens clamp (addressing CHANGES_REQUESTED / main drift)Rebuilt tip-of-main ( Dropped (now on main)
Kept (this PR only)
Local gates (on this tip)
Please re-run CI on the force-pushed head. Happy to split further if review still wants sampler kernels alone vs docs. |
CMake already listed rocm_sample.hip (V1 sampler) but the TU was missing from the tree — break configure. Add the file matching mudler#234 tip. PREFILL_GEMM_M env clamp 2048→8192 for lab A/B; default stays 2048. Hot-expert sizes at ~11k already fit 2048 (4096/8192 measured flat). FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
42ba78e to
8ea62b2
Compare
|
Rebased onto latest |
Tip-of-main rebuild (back on track)Rebuilt as single commit on current
Please re-run CI. Prior red agent-record/pr-size were base-not-ancestor. |
8ea62b2 to
a418791
Compare
a418791 to
0ed9650
Compare
ROCm/Gemma4 half of the mudler#228 split. No OpenAI SSE/serving changes (see mudler#316). No V1 sampler registration (see mudler#234). Includes dual-GPU FP8 resident experts, peer mix helpers, SharedK-WMMA prefill, decode KV-split path, fused_ops seam. Review fixes absorbed in this tip: - Restore HIPBLAS_V2 (ROCm 6.x mudler#201/mudler#132) — was clobbered in megapr - Do NOT flip platforms/rocm support_static_graph_mode (stays main) - Host-side #if VT_ROCWMMA_OK around SharedK-WMMA launches (no empty kernel launch on non-gfx1200/1201 slices) Still follow-ups (called out in PR body): HIP guards in models/, MoE policy FreeBytes/FP8_NATIVE defaults, peer weight_id / hscale, tests/spec. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
0ed9650 to
f82b26b
Compare
ROCm/Gemma4 half of the mudler#228 split. No OpenAI SSE/serving changes (see mudler#316). No V1 sampler registration (see mudler#234). Includes dual-GPU FP8 resident experts, peer mix helpers, SharedK-WMMA prefill, decode KV-split path, fused_ops seam. Review fixes absorbed in this tip: - Restore HIPBLAS_V2 (ROCm 6.x mudler#201/mudler#132) — was clobbered in megapr - Do NOT flip platforms/rocm support_static_graph_mode (stays main) - Host-side #if VT_ROCWMMA_OK around SharedK-WMMA launches (no empty kernel launch on non-gfx1200/1201 slices) Still follow-ups (called out in PR body): HIP guards in models/, MoE policy FreeBytes/FP8_NATIVE defaults, peer weight_id / hscale, tests/spec. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
f82b26b to
a1adc5e
Compare
ROCm/Gemma4 half of the mudler#228 split. No OpenAI SSE/serving changes (see mudler#316). No V1 sampler registration (see mudler#234). Includes dual-GPU FP8 resident experts, peer mix helpers, SharedK-WMMA prefill, decode KV-split path, fused_ops seam. Review fixes absorbed in this tip: - Restore HIPBLAS_V2 (ROCm 6.x mudler#201/mudler#132) — was clobbered in megapr - Do NOT flip platforms/rocm support_static_graph_mode (stays main) - Host-side #if VT_ROCWMMA_OK around SharedK-WMMA launches (no empty kernel launch on non-gfx1200/1201 slices) Still follow-ups (called out in PR body): HIP guards in models/, MoE policy FreeBytes/FP8_NATIVE defaults, peer weight_id / hscale, tests/spec. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
Register temperature/top-k/top-p/min-p/penalties/masks/logprobs/random sample on ROCm so EngineCore no longer fatals with missing sample ops after prefill. RandomSample is a parallel gumbel-max block reduce (kBlock threads) — the previous 1-thread full-vocab scan collapsed temp>0 decode to ~1.8 t/s on Gemma-4 (V=262k) while greedy stayed ~45. Rebased onto current main; docs are additive only. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
a1adc5e to
b067cd3
Compare
ROCm/Gemma4 half of the mudler#228 split. No OpenAI SSE/serving changes (see mudler#316). No V1 sampler registration (see mudler#234). Includes dual-GPU FP8 resident experts, peer mix helpers, SharedK-WMMA prefill, decode KV-split path, fused_ops seam. Review fixes absorbed in this tip: - Restore HIPBLAS_V2 (ROCm 6.x mudler#201/mudler#132) — was clobbered in megapr - Do NOT flip platforms/rocm support_static_graph_mode (stays main) - Host-side #if VT_ROCWMMA_OK around SharedK-WMMA launches (no empty kernel launch on non-gfx1200/1201 slices) Still follow-ups (called out in PR body): HIP guards in models/, MoE policy FreeBytes/FP8_NATIVE defaults, peer weight_id / hscale, tests/spec. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
Lab live :8010 stack. Serve/parser portion also filed as mudler mudler#328. RandomSample parallel already on mudler#234; keep local park in sync. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Hermes:grok-4.5 [Hermes]
|
@localai-org-maint-bot Status check against CHANGES_REQUESTED / localai-bot notes:
Sanitize ASan/TSan remain ambient-main class. Product jobs (build-test-cpu, cuda-fat, vulkan) were green on the last full run. |
…eds a new op/use_a16 + stacking) Records the design finding for the remaining ~4x (host-sync-bound, 22k cudaStreamSynchronize): the grouped-MoE lever that would collapse the top_k x 3 per-expert GEMMs+drains/layer into 3 is NOT drop-in — vt::MoeGroupedGemmNvfp4 is W4A16 (bf16 activation, only scale2; the 35B uses it), wrong numerics for our TRUE-W4A4 experts. Options: (a) a new grouped fp4xfp4 (W4A4) op (fp4-quant the shared token activation once, grouped fp4xfp4 over top_k experts), or (b) the valid use_a16 W4A16 grouped mode (numerics change to verify) — both also need the per-expert Nvfp4Weight vector STACKED at load (like qwen3_5 A3 W2). Needs a spike (task mudler#234, converges with the pending GGUF device-resident-decode mudler#228). Docs + spec N5 updated. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
…nt alone is eager-slower; graph is the payoff) Completes the mudler#234 scoping with the decisive precedent from the DeepSeek-V4 device-decode campaign (Bricks A-D, state.md/CLAIM-DEEPSEEK-V4-DEVICE-DECODE): ds4 built exactly the device-resident decode (ForwardResidentDecodeGguf, whole step as one async device chain, per-op syncs dropped) and it was TOKEN-IDENTICAL but EAGER-SLOWER (-20%) — the ~1700 small device-kernel launches/step leave the GPU ~45% idle in host-launch gaps. The sync-drop is a PREREQUISITE; the PAYOFF is the decode CUDA-graph (one cudaGraphLaunch). So the remaining Laguna N5 levers 4+5 are ONE campaign (device-resident THEN graph), the reuse target is qwen3_5's Dev/Nvfp4Dev/ResidentNvfp4/device-SwiGLU machinery, and per the ds4 projection a graph reaches ~10-13 tok/s (clears our ~4.5, may still trail vLLM 18.8). It also lifts the GGUF path (mudler#228 = the same lever). Spec/docs updated; this is a spike-first campaign. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
…e rest is kernel efficiency Derived from the measured lever-1 state: at 0.20 s/tok the GPU is ~87% busy, so GPU compute is ~0.174 s/tok and host gaps only ~0.026. A PERFECT decode graph (removing all host gaps) caps at ~5.9 tok/s — it clears our ~4.5 but is still 3.3x short of vLLM's 18.8. So the graph is NECESSARY BUT NOT SUFFICIENT for parity; the remaining 3.3x is KERNEL EFFICIENCY: our enabled native fp4 MMA runs ~302us per M=1 expert GEMM (tensor-core tiles waste the single decode row) vs vLLM's tuned cutlass sm120a fp4 GEMM (MatmulNvfp4CutlassModel/DirectD) + FUSED norm+quant / silu+quant kernels. True Laguna-NVFP4 parity is TWO campaigns: (A) device-resident forward + decode graph -> ~5.9 tok/s; (B) route experts to cutlass DirectD + adopt the fused ops + an M=1-tuned fp4 GEMV -> the remaining 3.3x. This materially corrects the earlier "graph is the payoff" framing (the graph alone does not reach 18.8) and is critical for planning mudler#234/mudler#228. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
… 18.8 kernel) Implements N5 campaign-B (task mudler#234): LagunaMoeResidentMarlin + a BuildLagunaMoeMarlinResident that repack the routed experts into the resident Marlin layout and run them through vt::MoeGroupedGemmNvfp4Marlin — the 1:1 lift of vLLM's moe_wna16_marlin_gemm, i.e. the EXACT W4A16 kernel vLLM's 18.8 tok/s bar runs (VLLM_TEST_FORCE_FP8_MARLIN=1). Marlin is low-M-optimized (decode/batch-1), unlike the tensor-core W4A4 GEMMs that waste M=1 tile rows. Reuses the SHARED primitives (dense_nvfp4::Dev/DBuf/ResidentNvfp4/MakeTensor + vt::cuda Marlin repack/align ops + vt::MoeGroupedGemmNvfp4Marlin), mirroring qwen3_5.cpp's validated BuildMoeMarlinResident + MoeBlockFusedMarlinCuda (split-w13 layout, single-token T=1, top_k=Pk). The SACRED 27B/35B path is BYTE-UNTOUCHED (Laguna-local reconstruction over shared ops). Uses scale2 (W4A16 weight global scale), ignoring the W4A4 activation fields — exactly vLLM's config. Gated OFF by default (VT_LAGUNA_MARLIN_MOE=1 opt-in) until the DGX near-tie + ncu gate lands, so the current default GEMV path is unchanged (zero regression risk). COMPILES CLEAN on the GB10 sm_121a CUDA build (VT_MARLIN_NVFP4). OWED: DGX near-tie vs the vLLM-Marlin golden + kernel-duration ncu + same-binary A/B, then flip default-ON per parity-enablers-ship-as-defaults. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
…rst-forward) mudler#234 item (1): LagunaBuildMarlinResidents pre-builds all 48L×256E routed-expert Marlin residents at load time (called from examples/laguna_gen after the NVFP4 load), mirroring vLLM's process_weights_after_loading (marlin_utils_fp4.py), so the repack is a one-time load cost instead of a ~216s first-token TTFT spike. No-op unless VT_LAGUNA_MARLIN_MOE=1 + GPU (+ VT_MARLIN_NVFP4); the forward's lazy `if(!mr.ready) Build…` then finds every resident ready. Also fixes an anon-namespace linkage bug the first build surfaced: the public LagunaBuildMarlinResidents (declared in laguna.h) was defined INSIDE the file's anonymous namespace → internal linkage → -Werror=unused-function + the public decl unresolved. Moved the definition outside the anon namespace (external linkage); the anon-namespace Marlin helpers it calls stay visible via the anon namespace's implicit using-directive. BUILD CLEAN + links on GB10 sm_121a (main.cpp recompiled, laguna-gen linked), default-OFF. Runtime prewarm-fires-at-load timing UNVERIFIED this session (repeated ssh-drops on the shared box ate the run capture) — but it cannot regress: if the prewarm ever no-ops, the forward's lazy build (validated 3x at ~10 tok/s golden-matching) is the fallback. Owed: one clean confirmation run that prefill drops (build moved to load) → then flip default-ON. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
…stant Three open PRs each grew their own workaround for the same defect, with three different answers: mudler#227 clamps to 16, mudler#234 and mudler#228 to 4096. All three would conflict with each other, and all three truncate. This fixes it once, in the one place that owns the translation. Hermes and some OpenAI clients send `max_tokens: -1` for "no client-side limit". `SamplingParams::PostInit` rejects anything below 1, which is what made a clamp look necessary — but a constant is the wrong answer, because "unlimited" already has a correct representation here: UNSET. The engine then generates to `max_model_len - seq_len` (`v1/engine/input_processor.cpp:175-180`, a 1:1 mirror of vllm `input_processor.py:317-321`), which is exactly what the client asked for. With a constant, a client asking for unlimited against a 32k model gets 4096 (or 16) tokens back with `finish_reason=length`, indistinguishable from a real limit it set itself. It also hardcodes a policy number into the protocol layer, where `VT_SERVER_MAX_NEW_TOKENS` is already the tunable. Both request paths are covered: `/v1/completions` (`max_tokens`) and `/v1/chat/completions` (`max_completion_tokens` preferred over `max_tokens`, matching chat_completion/serving.py:299). A positive value is honoured unchanged, and a serving-resolved default still wins over an unset request value. RED before, GREEN after, same test and command, on dgx with the new test built against UNPATCHED main: before: test case THREW exception: max_tokens must be at least 1, got -1 29 cases | 28 passed | 1 failed; 175 assertions | 2 failed after: 29 cases | 29 passed | 0 failed; 179 assertions | 0 failed `test_openai_protocol` gains six subcases pinning the semantics: -1 and 0 unset on both paths, a positive value unchanged, and -1 yielding to a serving default when one is supplied. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
…ans UNSET Three open PRs each grew a workaround for the same defect with three different answers: mudler#227 clamps to 16, mudler#234 and mudler#228 to 4096. They would conflict with each other and all three truncate. Fixed once, in the layer that owns the translation, so those PRs can drop it and rebase. Hermes and some OpenAI clients send max_tokens=-1 for "no client-side limit"; PostInit rejects <1, which is what made a clamp look necessary. But "unlimited" already has a correct representation: UNSET, which routes to max_model_len - seq_len (input_processor.cpp:175-180, mirroring vllm input_processor.py:317-321). A constant silently truncates exactly the long-context request that asked to be left unlimited. RED before / GREEN after on dgx, the new test built against UNPATCHED main: before: THREW 'max_tokens must be at least 1, got -1'; 28/29 cases, 2 failed after: 29/29 cases, 179/179 assertions Covers both request paths and documents the semantics in docs/USAGE.md. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
… ROCm (mudler#234) External contribution from Don Mirror (@bakon11). Lab evidence: dual R9700 gfx1201. The bug it closes: ROCm registered no sampling ops at all, so EngineCore fatally hit "no kernel for op" immediately after prefill on AMD. Every V1 sampler op is now registered for DeviceType::kROCM -- temperature, top-k/top-p, probs, logprobs, random sample, penalties, min-p, logit bias, allowed-token-ids and the sparse bad-words token mask. The PR's red agent-record and pr-size checks were the same FORK ARTIFACT as mudler#283: both refused with "base must be an ancestor of head" because the branch was never rebased, so neither could compute a range. sanitize-cpu (address,undefined) is the pre-existing main baseline. No substantive job was red, and no CI job compiles ROCm at all (there are no AMD runners), so the green checks are orthogonal either way. Because CI cannot compile or run this, I verified the port mechanically rather than by reading it. src/vt/rocm/rocm_sample.hip was normalized against src/vt/cuda/cuda_sample.cu (namespace, hip/cuda prefixes, kernel suffixes and whitespace folded away) and every remaining difference is cosmetic: entry-point names, error-string prefixes, line wrapping, and CUDA's trailing explanatory comments. The numerics are IDENTICAL, including the pieces that decide token identity -- SplitMix64, ExpNoise's (r >> 11) + 1 over 9007199254740993.0 mantissa construction, the gumbel/exp-noise argmax, the temperature's !all_random && t < kSamplingEps guard, and flashinfer's two-pivot sort-free bracket search with the same kThreshMaxIter = 64 and the same min_gt_low / max_le_high snapping. This is a genuine 1:1 port, not a reimplementation, which is what the porting rule requires. Reviewed and accepted as-is: RandomSampleK launches <<<n, 1>>>, one thread per row scanning the whole vocab serially. That is not a defect here -- cuda_sample.cu launches it exactly the same way, so the port is faithful and a change would be a divergence. It is a real optimization target for whoever takes ROCm sampling past correctness-grade. Tests skip cleanly via HasRocm() when no ROCm backend is registered, so they are inert on CPU CI and assert real CPU-vs-ROCm parity on AMD hardware. docs/USAGE.md's non-positive max_tokens claim is backed by code already on main (protocol.cpp:525), not by anything this PR asserts without landing. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
kCausalConv1dFwd / kCausalConv1dUpdate for ROCm (rocm_gdn_conv.hip), family 2 of the gfx1100 GDN slice, hand-translated from the CUDA donors. Conflicts resolved by union, all additive-vs-additive: the ROCm source list in CMakeLists.txt gained rocm_sample.hip (mudler#234, already on main) and rocm_gdn_state.hip (mudler#334) independently, and both the FEATURES ROCm row and the coordination claim table are keyed records where the two sides touch different keys. Refs mudler#336, mudler#41. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4-flash [edit bash]
Summary
Makes Hermes Agent ↔ local Gemma-4-26B MoE FP8 on AMD ROCm (RDNA4) finish chat turns and decode at usable speed when
temperature > 0.Public shout-out context: @anothervariable — Gemma 4 26B MoE on AMD ROCm via vllm.cpp.
Root causes (lab)
1) Missing ROCm V1 sampler ops (engine death)
After a large Hermes SOUL prefill (~38k tokens), sampling called V1 ops registered on CUDA/CPU only:
kApplyAllowedTokenIds(40)no kernel for op 40 on device type 5kApplyTemperature(30)no kernel for op 30request submitted to a stopped AsyncLLMROCm device type = 5. Missing registration = hard engine death, not a soft 4xx.
2)
RandomSampleKserial full-vocab scan (decode cliff)HIP port initially matched CUDA's
<<<n, 1>>>gumbel-max. On gfx1201 with Gemma-4 V=262144, every temp>0 token paid ~0.5s in sampling → Hermes decode ~1.75 t/s vs greedy ~40–50 t/s.Fix: parallel block reduce, launch
<<<n, kBlock>>>withkBlock=256.Changes
src/vt/rocm/rocm_sample.hip— HIP V1 sampler surface:rocm_ops.hip/rocm_dense_basic.hip— register those +ApplyAllowedTokenIds/ApplyTokenMaskmax_tokens=-1(Hermes “unlimited”) → map toVT_SERVER_MAX_NEW_TOKENSdefault (4096) beforeSamplingParams::PostInitrejects<1ENVIRONMENT.md,FEATURES.md,USAGE.md(sampler note)No
vt::rocmcalls frommodels/(device-leakage clean). CUDA still has 1-threadRandomSample(hidden on fat SMs / smaller V); ROCm deliberately diverges for gfx1201.Lab verification (2× Radeon AI PRO R9700, gfx1201, ROCm 7.2)
Also: title-gen allowlist + temp/top_p +
max_tokens=-1→ 200; Paris smoke OK.Binary: CMake
OUTPUT_NAMEisvllm-serverunderbuild-*/examples/.Land order
Follow-ups (not this PR)
cuda_sample.cu)Test plan
python3 scripts/check-device-leakage.pypython3 scripts/check-env-doc.pypython3 scripts/check-doc-checkpoint.py --base origin/main --head HEADpython3 scripts/check-pr-size.py(under budget):8010Gemma-4-26B-A4B-it-fp8 dual-GPU: temp 0 vs 0.7 A/B