Skip to content

feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA prefill (RDNA4) - #228

Closed
bakon11 wants to merge 5 commits into
mudler:mainfrom
bakon11:feat/gemma4-fp8-prefill-reliability
Closed

feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA prefill (RDNA4)#228
bakon11 wants to merge 5 commits into
mudler:mainfrom
bakon11:feat/gemma4-fp8-prefill-reliability

Conversation

@bakon11

@bakon11 bakon11 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Gemma-4-26B MoE ROCm path for dual-GPU (gfx1201 lab):

  • Multi-EOS from config.jsongeneration_config.json ([1,50,106])
  • Host expert LRU + dual-GPU native FP8 resident packs
  • Fused ExpertGeGLU FP8 decode (M=1)
  • Prefill: group-by-expert batch (T≥64), FP8 channel GEMM M>1 (weights stay FP8 on GPU), device gather + weighted scatter (no host weight dequant / no host hacc)
  • Portable vt::fused_ops seams — no vt::rocm from models/

Lab (2× R9700, ROCm 7.2.4)

Prompt Prefill tok/s Quality
~800 ~167 READY
~6k ~123 READY
~10k ~192 READY
Paris warm Paris

hipBLASLt W8A8 microbench still slower than BF16 GemmEx on gfx1201 for pure matmul — not default.

Size

Over the ~900 product-line ratchet (MoE + HIP kernels). Happy to split further if maintainers prefer (decode/resident vs prefill batch).

Supersedes

Supersedes the bulk of #205 content (rebased to current main as a single tip commit). Please close #205 in favor of this + the serve reliability PR.

Test plan

  • check-device-leakage
  • check-env-doc / FEATURES / USAGE / ENVIRONMENT
  • Live :8010 dual FP8 resident smoke (Paris/READY/prefill rates)
  • CI

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Update after live Hermes proof (RDNA4 / dual R9700):

Full agent path works end-to-end (SOUL ~38k prefill → streamed decode → real reply). The blocking EngineCore fatals after prefill were missing ROCm V1 sampler ops (op 30 temperature, op 40 allowed_token_ids) + Hermes max_tokens=-1.

That fix is split out as a small tip-of-main PR for fast merge:
#234

This #228 remains the larger FP8 resident / fused decode / GPU prefill-MoE package. Prefer landing #234 first, then rebase this head.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

TESTERS WANTED — clear split: FP8 (lab) vs BF16/FP16 MoE (community)

What this PR is

FP8-first Gemma-4 MoE on ROCm: native FP8 resident experts, fused FP8 decode helpers, GPU prefill-batch MoE, etc. Lab-proven on 2× R9700 gfx1201 with Hermes-scale prompts (slow decode at 38k, but correct).

What it is not

A claim that every kernel here is the BF16 path. FP8 weight GEMM / FP8 resident upload do not apply to BF16 weights.

What does still matter for BF16/FP16 MoE testers

If you run BF16 or FP16 MoE on ROCm, you still want:

Land first Why
#234 Full V1 sampler on ROCm + Hermes max_tokens=-1 (EngineCore op 30/op 40 fatals are dtype-agnostic)
#227 KV admit fail-fast + clamps (large-prompt hangs are dtype-agnostic)

Optional on this PR after those: any shared MoE control flow you can exercise in BF16 (group-by-expert prefill batch, dual-GPU placement) — report whether BF16 hits the same batch path or stays on serial expert loops.

Please do not treat as MoE validation

  • Dense Gemma-4-12B BF16 — no experts

Ideal tester hardware

  • BF16/FP16 Gemma-4-26B MoE (or similar) checkpoint
  • Multi-GPU ROCm with headroom beyond ~52 GiB weights
  • Host RAM ≫ 30 GiB preferred for clean loads

Report template

Prefer merge order: #234#227 → rebase this FP8 MoE package.

Thanks — FP8 is the consumer-RDNA4 daily driver; BF16/FP16 MoE needs wider silicon than this lab can soak alone.

@localai-org-maint-bot localai-org-maint-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lab evidence is useful, but this introduces a large FP8 resident/prefill execution surface without automated kernel/model parity tests; the only added tests cover HF config parsing. Please add focused parity coverage for resident expert placement, fused decode, and grouped prefill (including multi-device behavior or a testable dispatch seam), then rebase after #234/#227 as proposed and clear the current repository gates. The current single-hardware smoke is not sufficient protection for this execution-path change.

@localai-bot

Copy link
Copy Markdown
Collaborator

Reviewed. The ROCm work looks solid and it builds clean on CUDA — but I'm asking for this to be split before it lands, and the reason is reviewability rather than anything wrong with the code.

Verified so far (GB10 sm_121a, rebased on current main)

BUILD_OK          full CUDA build, 0 errors
test_hf_config    192/192

SACRED gates are queued behind other GPU work on the box; I'll post them when they land. Note this is the first time any CI has run against this branch at allgh pr checks 228 reports no checks, so before my build nothing had compiled it.

The blocking concern: 16 commits, 7 of them research(rocm)

8f86f58d research(rocm): direct-store concurrent WMMA flash (BM32)
696d79e5 research(rocm): concurrent WMMA QK — LDS limit, not mma hang
9e59b655 research(rocm): SharedK prefill + profile — QK compute-bound
2f69efd4 research(rocm): flash dispatch split — scalar tile vs 1-wave WMMA
76aaef2a research(rocm): 1-wave/CTA WMMA flash prefill (gfx1201-safe)
07a67548 research(rocm): rocWMMA QK in flash tile (gfx1201) + proven microbench
7b47381b research(rocm): SGLang-style flash tile prefill (opt-in) + steal base

Those are an exploration log — LDS limits, hangs, dispatch experiments, microbenches. That's genuinely valuable work and the findings deserve to be kept, but as a spec/record, not as shipping commits: 4,783 lines mixing exploration with production changes can't be reviewed as one unit, and nobody can later tell which commit is load-bearing and which was a dead end that happened to leave code behind.

Concretely, I'd like:

  • One PR with the shipping perf work — the FP8 resident path, the fused decode, the GPU prefill MoE, BlockSumFast, the ExpertGeGLU fusion. That I can gate and merge quickly.
  • The research findings folded into .agents/specs/ with the measurements and the dead ends named, which is where the "multi-wave mma hangs gfx1201" result actually belongs so the next person doesn't rediscover it.
  • The dual-stream commit (b4e78df0, default off) can ride with the shipping PR since it's opt-in.

Third copy of the same max_tokens defect

if (req_max.has_value() && *req_max < 1) req_max = 4096;
if (req_max.has_value() && *req_max > 4096) {
  // soft cap; serving_chat may clamp further via VT_SERVER_MAX_NEW_TOKENS
}

This same edit is now in #227 (clamps to 16), #234 (4096), and here (4096) — three PRs patching one spot, which will conflict with each other. As noted on the others: -1 should become std::nullopt, which routes to the path we already have mirroring vLLM 1:1 (input_processor.cpp:175-180input_processor.py:317-321) and generates to max_model_len - seq_len. A hardcoded ceiling silently truncates the long-context request the client explicitly asked to leave unlimited. The second if is also dead code — its body is only a comment.

Pick one PR to own that fix and drop it from the other two.

Smaller notes

None of this is a criticism of the engineering — the gfx1201 findings are exactly the kind of thing worth having written down. It's about landing it in pieces each of which can be gated on its own.

localai-bot pushed a commit that referenced this pull request Aug 10, 2026
…stant

Three open PRs each grew their own workaround for the same defect, with three
different answers: #227 clamps to 16, #234 and #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: Claude:claude-opus-5 [ClaudeCode]
localai-bot pushed a commit that referenced this pull request Aug 10, 2026
…ans UNSET

Three open PRs each grew a workaround for the same defect with three different
answers: #227 clamps to 16, #234 and #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: Claude:claude-opus-5 [ClaudeCode]
@localai-bot

Copy link
Copy Markdown
Collaborator

Heads-up: the max_tokens clamp is now fixed on main as 2d2bdd04 (row/SERVE-MAXTOKENS-UNSET), so please drop it from this PR and rebase — otherwise the three PRs conflict over one spot.

A non-positive max_tokens (or max_completion_tokens) is now treated as unset on both request paths, which routes to max_model_len - seq_len in input_processor.cpp:175-180, mirroring vllm input_processor.py:317-321. No constant, no truncation of 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 assertions failed
after:  29/29 cases, 179/179 assertions

test_openai_protocol now pins the semantics (−1 and 0 unset on both paths, positive unchanged, and −1 yielding to a serving default). Semantics documented in docs/USAGE.md.

Everything else in your PR stands — this is only about the shared hunk.

@localai-bot

Copy link
Copy Markdown
Collaborator

Reviewed at 4e75e18d, merged onto current main and built for CUDA on a GB10 (sm_121a). Your shared-code changes do not disturb the backends that already work:

test_qwen3_paged_engine   184/184
test_qwen27_paged_engine  235/235   (SACRED)
test_hf_config            192/192

That was the open question from my last pass, so it is now answered and off the table. I can't review or run the ROCm kernels (no AMD GPU on this side), but the ~1600 lines under src/vt/rocm/ are device-local and gated by your own hardware — that half is yours to own.

The generation_config.json eos finding is real, and it was OUR bug

This is the most valuable thing in the PR and it is worth separating from the ROCm work. You are right that we were dropping Gemma-4's third eos id. I traced it: InputProcessor::generation_config_eos_ids_ is named for generation_config.json but was populated entirely from config.json — nothing in the LLM path ever opened generation_config.json (only Parakeet's loader did). So upstream's "secondary eos ids" were, in our port, a second copy of the primary list. Any chat model whose generation_config adds a turn-level stop ran past it to the length cap.

But the fix belongs one layer up from where you put it. Merging into cfg.raw["eos_token_id"] lands the ids in the field that supplies the primary eos, and we already have a faithful 1:1 port of update_from_generation_config (input_processor.cpp:72) that gates secondary ids on ignore_eos. Two consequences:

  1. ignore_eos=true can no longer generate past those ids — upstream (sampling_params.py:645-655) keeps that gate.
  2. The primary eos is generation_config_eos_ids_.front(). Your merge writes back a std::set-ordered union, so for any checkpoint whose config.json eos array is not already ascending, the sort silently changes which id is primary.

I've landed the corrected version separately (HfConfig::generation_config_eos_ids as its own field, raw untouched, unioned by append so the primary keeps its position) with red-first tests on both suites — credited to this PR for the find. Please drop the hf_config.cpp / FEATURES.md eos hunks on your next rebase; they'll conflict otherwise, and the behavior you want will already be on main.

Still the same blocker: this needs splitting

5,364 additions across 34 files, and six more perf commits landed while I was reviewing. I can gate it, but I cannot meaningfully review it as one unit, and neither can anyone else. Concretely, these are independent and each mergeable on its own:

  • eos parity — now handled on main; drop it.
  • prefill progress tok_s instrumentation (scheduler.cpp) — self-contained, reviewable in minutes.
  • shared Gemma-4 model changes (gemma4.cpp, gemma4_moe.cpp, ~1,500 lines) — this is the part that needs real review, and it's the part currently buried.
  • ROCm kernels (~1,600 lines) — your hardware, your gates.

Split in that order and I'll merge the first three quickly. One note on the scheduler hunk while it's fresh: MaybeLogPrefillProgress now also runs from update_from_output, so its static std::unordered_map<std::string, State> states is touched on every step for every request and is still never erased when a request finishes. The leak predates you — but this change makes it grow faster on a long-lived server, so an erase on completion would be a good thing to fold in.

localai-bot pushed a commit that referenced this pull request Aug 10, 2026
fix(parity): read generation_config.json eos ids

We only ever read config.json. `generation_config_eos_ids_` was named for
generation_config.json but populated from config.json's `eos_token_id`, and
nothing in the LLM path opened generation_config.json at all — only Parakeet's
loader did. Upstream reads both (try_get_generation_config for the default
--generation-config auto, then sampling_params.py:645-655 merges the ids into
stop_token_ids), and the two files routinely disagree: Gemma-4-26B ships
config.json [1, 106] against generation_config.json [1, 106, 50]. Id 50 never
stopped a generation, so chat models ran to the length cap.

Found via #228, which hit it on Gemma-4 but fixed it in raw["eos_token_id"] —
the field that supplies the PRIMARY eos, which our existing 1:1 port already
gates on ignore_eos. These are SECONDARY stop ids. This carries them in their
own field, leaves raw untouched, and unions by append so the primary keeps its
position.

Red first: both suites fail 2 assertions each without the fix.

  test_qwen3_paged_engine   184/184
  test_qwen27_paged_engine  235/235   SACRED
  test_qwen36_paged_engine  315/315   SACRED
  test_hf_config            194/194
  test_input_processor       64/64

Both SACRED gates matter here: honoring new stop ids could have shortened
gate-model generations and drifted the goldens. They did not move.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
@localai-bot

Copy link
Copy Markdown
Collaborator

Follow-up: the corrected eos fix is now on main as fb2dfac7 (#262), credited to this PR for the find. Please drop the hf_config.cpp and docs/FEATURES.md eos hunks when you next rebase — the behavior you wanted is already there, and those two files will conflict otherwise.

rohitpaul pushed a commit to rohitpaul/vllm.cpp that referenced this pull request Aug 10, 2026
…config.json

`InputProcessor::generation_config_eos_ids_` is named for
generation_config.json but was populated entirely from config.json's
`eos_token_id`. Nothing in the LLM path read generation_config.json at all
(only Parakeet's loader did), so upstream's "secondary eos ids" were, in our
port, just a second copy of the primary list.

Upstream loads both. ModelConfig.try_get_generation_config (vllm/config/
model.py) reads generation_config.json whenever --generation-config is "auto"
(the default) or "vllm", and SamplingParams.update_from_generation_config
(vllm/sampling_params.py:645-655) merges its ids into stop_token_ids. The two
files routinely disagree: Gemma-4-26B ships

  config.json             eos: [1, 106]
  generation_config.json  eos: [1, 106, 50]

so id 50 — a turn-level stop — never terminated a generation for us. The
symptom is a chat model that runs past its stop token to the length cap.

Found via mudler#228, which hit this on Gemma-4 and fixed it by merging the ids into
`cfg.raw["eos_token_id"]`. That lands them at the wrong layer: raw's
`eos_token_id` supplies the PRIMARY eos id, which our existing 1:1 port of
update_from_generation_config already gates on `ignore_eos`, while these are
SECONDARY stop ids. Rewriting raw would make them unconditional, and — since
the primary is `generation_config_eos_ids_.front()` on the list path — a
re-sorted union could silently displace which id is primary.

So this carries the field separately (`HfConfig::generation_config_eos_ids`),
leaves `raw` untouched, and unions by append so the primary keeps its
position. The existing ignore_eos gates then apply unchanged: the ids reach
stop_token_ids only when ignore_eos is false, and all_stop_token_ids (min_tokens
masking) regardless — which is exactly sampling_params.py:653.

Red first: both new suites fail 2 assertions each against the current
implementation and pass with it. A malformed or absent generation_config.json
is a silent no-op, mirroring try_get_generation_config returning {} rather
than raising.

  test_hf_config         194/194   (+5 subcases)
  test_input_processor    64/64    (+2 subcases)

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Status (lab + PR hygiene)

Land order: #234 (ROCm V1 sampler, rebuilt tip-of-main, clamp dropped) → #227 (KV fail-fast, rebuilt tip-of-main, prefix probe + clamp dropped) → then this #228 rebase.

Maintainer notes absorbed for the next #228 push:

Lab prefill (not blocking this PR thread): SharedK-WMMA + PREFILL_GEMM_M=20482010 eng @~11k on dual R9700 FP8 MoE; still working toward ~3k. Will rebase #228 onto main after #234/#227 clear conflicts.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Lab tip 9a009eb6 now includes SharedK-WMMA + GEMM_M=2048 + PEER_ACT default ON (~2057 eng @11k). Body refreshed. Hold merge until #234 then #227 land; then full rebase + docs/pr-size pass. Best recipe handoff on lab host: bash ~/llms/scripts/vllm/start-gemma4-fp8-8010.shhttp://127.0.0.1:8010/v1.

@bakon11 bakon11 changed the title feat(gemma4/rocm): native FP8 resident, fused decode, GPU prefill MoE feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA prefill (RDNA4) Aug 10, 2026
Rebuild mudler#228 onto current main. Drop workarounds that already landed:

- max_tokens clamp / -1→4096 (main 2d2bdd0 SERVE-MAXTOKENS-UNSET)
- generation_config eos merge (main fb2dfac / mudler#262)

Keep operator VT_SERVER_MAX_NEW_TOKENS for *positive* oversized requests
only. Non-positive remains UNSET → max_model_len - seq_len.

Includes lab wins: SharedK-WMMA, PREFILL_GEMM_M=2048, PEER_ACT default ON,
SSE keepalives + deferred add_request for long Hermes prefill, prefill
progress tok_s, rocm_sample.hip for V1 sampler ops.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11
bakon11 force-pushed the feat/gemma4-fp8-prefill-reliability branch from 9a009eb to e6c8c4e Compare August 10, 2026 15:50
FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on tip-of-main — clamp + eos dropped

Per maintainer note (2d2bdd04 / row/SERVE-MAXTOKENS-UNSET) and follow-up on eos (fb2dfac7 / #262):

Dropped from this PR Now owned by main
protocol.cpp -1→4096 / soft cap Non-positive max_tokens = UNSETmax_model_len - seq_len
serving_chat clamp on before < 1 Only VT_SERVER_MAX_NEW_TOKENS for positive oversize (unchanged policy)
hf_config / FEATURES multi-EOS merge generation_config eos via #262

Head: e6c8c4e9 (+ follow-up newline fix) — single rebuild tip on origin/main 848d4a87 (not a multi-commit rebase fight).

Still in this PR: FP8 resident MoE, SharedK-WMMA prefill, PEER_ACT default ON, GEMM_M=2048, SSE keepalives + deferred add_request (Hermes long-prefill), prefill progress tok_s, rocm_sample.hip.

Land order preference unchanged: #234 sampler → #227 KV fail-fast → this #228 (sampler TU also present here so either order can work after a final rebase).

Local: check-device-leakage / check-env-doc / check-public-doc-tables green; HIP server build OK on lab dual-R9700.

Tip CMake had dropped main's DSpark sources and HIP -O1 floor (mudler#132).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the rebuild tip: CMakeLists restored from main (DSpark + HIP -O1 floor kept) with only rocm_sample.hip added — HIP server links clean on lab. Tip now 136d9d74.

@localai-bot

Copy link
Copy Markdown
Collaborator

Thanks for this — there's real engineering in here, and the FP8 resident-expert design plus the SharedK/WMMA prefill is exactly the direction the ROCm lane needs. I reviewed it in depth rather than landing it, and I'm asking for a split instead of a merge. Reasons below, with the evidence for each.

I want to be upfront about a limit that shapes everything: no CI job compiles ROCm and there is no AMD GPU on the maintainer box. So the three green build checks prove only that x86 CPU still builds — roughly 4,400 lines of HIP in this PR have never been compiled by anyone but you. That's why the ROCm half needs your hardware to land, and why I'm not going to "fix it up and merge" blind.

1. The three red checks have two root causes, and neither is a race

build-test-cpu and sanitize-cpu (thread) are the same failure — test_openai_api_server, 3 cases / 4 assertions. I grepped the TSan job log: ThreadSanitizer appears zero times. There's no data race; the thread lane is just the second lane that builds with VLLM_CPP_SERVER=ON.

documentation-checkpoint is mechanical: check-doc-checkpoint.py runs per commit, CMakeLists.txt is in both USER_USAGE_FILES and LANDING_SOURCE_FILES, and commit 136d9d74 touches only CMakeLists.txt while docs/USAGE.md moved in e6c8c4e9. Squashing the three commits into one fixes it — verified by building a squashed commit with git commit-tree and re-running the checker (exit 0).

2. The SSE rewrite regresses streaming for every model, on every backend

Same-binary A/B, identical tree, only the six serving files swapped:

Arm Result
PR head, all 33 files 3 failed / 50
PR head, only serving_chat.cpp, serving_completion.cpp, serving_utils.{h,cpp}, request_logger.{h,cpp} reverted 50 passed / 50, 624/624 assertions

Three distinct defects:

  • Streaming stops streaming. serving_completion.cpp:118-127 swaps the blocking engine_.get_output() for get_output_nowait() + sleep_for(50ms). RequestOutputCollector is single-slot and DELTA outputs coalesce (output_processor.h:71-74), so the consumer is asleep almost always and a 20-token stream collapses to one content frame + [DONE], finish_reason already set on the first chunk. TTFT becomes full generation time — the opposite of the goal. A real fix needs a timed wait (ready_.wait_for(deadline)), not a poll.
  • Continuous usage dropped from the role frame — the include_continuous_usage buffering block and frame.usage = UsageInfo{...} were deleted. That's ported upstream behavior (serving.py:404-802).
  • MaybeSsePing emits ":\n\n" and the data frame as one chunk, breaking one-frame-per-chunk; chunk.substr(6) then lands mid-"data". CI was fast enough never to fire it.

The keepalive/SSE idea is legitimate and worth having — it just needs its own PR, against the streaming tests it currently breaks.

3. Two changes reach much further than "gemma4/ROCm"

4. ROCm-tuned policy applied to CPU and CUDA, unguarded

gemma4_moe.cpp has exactly two preprocessor lines — #ifndef VLLM_CPP_HIP at :1570 and #endif at :1624 — wrapping only link stubs. Lines 1–1569, the whole rewritten RunGemma4Moe, compile and run everywhere:

  • :497 if (!FreeBytes(d, &free_b)) return false; — only ROCm overrides Backend::DeviceMemoryInfo (backend.h:81 returns false), so MakeRoom always fails on CUDA/CPU and kills the device expert cache that worked before. Every expert of every top-k falls back to a host H2D.
  • :943 flips VT_GEMMA4_FP8_NATIVE from OFF to ON for all backends, changing the accumulation arm and therefore rounding — any gemma4 token-exactness gate moves.
  • :73 adds an unconditional per-expert Synchronize, commented as a ROCm crash workaround. On CUDA prefill that's ~2 full stream syncs per token per MoE layer.

5. Kernel-level findings I could not run, only read

These need your gfx1201 box to confirm or refute:

  • rocm_paged_attn.hip:7-10, 1322VT_ROCWMMA_OK is gated on __gfx1200__/__gfx1201__, and the entire PagedAttnPrefillSharedKWmma body including its final store sits in the #else. On gfx1100/gfx1151/gfx942 (all in VLLM_CPP_HIP_SUPPORTED_ARCHS, and VLLM_CPP_HIP_ARCHITECTURES defaults to empty/auto-detect) it compiles to (void) casts and never writes out — but the host dispatch is default-ON with no arch guard and then returns, so no fallback runs and hipGetLastError() is clean. That's silent garbage prefill attention. rocm_arch.h::CapabilityFromGcnArch already exists to gate it.
  • gemma4_moe.cpp:1450 — the peer-FP8 decode path omits weight_id, so the sticky dequant cache keys on a thread_local staging pointer that's identical for every expert. The function's own comment at :110-113 says the key must not be that pointer, and the prefill caller at :1156 passes fp8_weight_id correctly. Experts 2..7 would reuse expert 1's weights under their own router weights — coherent-looking wrong text.
  • gemma4_moe.cpp:753 + :821-838ApplyExpertScaleRwKernel mutates rw in place, then the fall-through copies the already-scaled rw and applies hscale again → routing weights become w·scale².
  • rocm_fp8_channel_gemv.hip:510-524 — pointer-table hipMemcpyAsync sources are caller host arrays (including &fp8_gu, a parameter address, and a stack const float wts[1]) that die immediately, with no sync before return. Whether ROCm's pageable staging masks it is a hardware question; by contract it's a use-after-free, and this repo has been bitten by exactly that before.
  • rocm_gemma4_experts.hip:163-168 — resident FP8 bases are hipMalloc'd and never freed, though gemma4_moe.h:52 promises teardown.

Credit where it's due, verified clean: FP8 scale semantics are correct throughout (per-output-channel, never per-K, gate/up halves get separate scales, fp32 accumulation); the e4m3fn bit decode matches OCP apart from NaN mapping to 0.0; rocm_sample.hip is a faithful line-by-line port of cuda_sample.cu with no new reassociation; Backend::DeviceMemoryInfo is a textbook additive virtual; and the default-ON SharedKWmma LDS budget does fit in 64 KB.

6. Process

No .agents/specs/* entry, no linked issue, and zero tests for 6,029 product lines. Policy is a committed spec before implementation and ported tests in the same change. The evidence table is prefill tok/s and "Paris/READY" — there's no vLLM denominator and no token-exact or ratified distributional gate, and correctness gates come before throughput here.

Worth noting check-device-leakage.py counts CUDA tokens only, so gemma4.cpp:57-62, 512-550 (which #include <hip/hip_runtime.h> and call nine raw HIP APIs from the device-agnostic model layer) passed through a genuine blind spot. I'll open a separate issue to add a HIP bucket — that's on us, not you.

What I'd suggest

Split it in two. The A/B shows that reverting just the six serving files makes this PR green on everything CI can see:

  1. SSE/keepalive PR — with a timed wait instead of the 50 ms poll, the role-frame usage restored, the ping as its own chunk, and tests.
  2. ROCm/gemma4 PR — with HIPBLAS_V2 restored, the support_static_graph_mode flip pulled out, the gemma4_moe.cpp policy changes guarded by #if defined(VLLM_CPP_HIP) (or treating unknown memory info as unlimited, as before), an arch guard on the default-ON WMMA dispatch, and a spec + issue.

I'd rather have this land properly than land fast, and the ROCm half genuinely needs your hardware to gate — that's the one thing I can't do from here. Happy to review either half quickly as you split them, and to pre-check anything CPU-side.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Status — hold merge; split next (back on track)

Land order in progress:

  1. feat(rocm): V1 sampling ops for Hermes EngineCore #234 — rebuilt tip-of-main single commit a418791e (local gates green; CI re-run)
  2. fix(server): fail-fast unschedulable KV waits #227 — rebuilt tip-of-main clean fail-fast only ac5725fb (no metrics/spec clobber; local gates green; CI re-run)
  3. This feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA prefill (RDNA4) #228hold. Will split per maintainer review before next push:
    • PR A: SSE/keepalive with timed wait + streaming tests (no 50ms poll)
    • PR B: ROCm/gemma4 only — restore HIPBLAS_V2, no unguarded graph flip, HIP-guard MoE policy, WMMA arch guard, spec/issue/tests

Lab decode WIP (BPE heap, structural-tag default-off, FP8 HW cvt, KV-split/slide, etc.) lives on bakon11 wip/lab-decode-20260810 — not in this PR until split/gates.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Split in flight

PR Scope
#234 ROCm V1 sampler
#227 KV fail-fast
#316 SSE keepalives (get_for timed wait)
new ROCm split feat/gemma4-rocm-fp8-split — FP8 MoE + SharedK-WMMA only

#228 = hold / close when replaced. No merge-as-is.

@bakon11

bakon11 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #317

This megapr head is behind main and carries mixed serve/docs/history noise.

Land path (split):

  1. feat(rocm): V1 sampling ops for Hermes EngineCore #234 — ROCm V1 sampler (includes parallel RandomSample; Hermes temp>0 decode fix)
  2. fix(server): fail-fast unschedulable KV waits #227 — fail-fast unschedulable KV waits
  3. fix(server): SSE keepalives via collector timed wait #316 — SSE keepalives
  4. feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA (split from #228) #317 — Gemma4/ROCm FP8 + SharedK-WMMA (this PR’s product payload)

Closing #228 in favor of #317 to avoid dual-tracking. Lab decode stack stays on bakon11 WIP branches, not this PR.

bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 11, 2026
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]
@bakon11

bakon11 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by split #317 (+ #234/#227/#316). See comment.

@bakon11 bakon11 closed this Aug 11, 2026
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 11, 2026
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]
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 11, 2026
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]
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 11, 2026
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]
localai-bot pushed a commit that referenced this pull request Aug 11, 2026
…dK-WMMA (#41)

Lands bakon11's work from PR #317, split out of #228. Merged --no-ff so the
contributor commits keep their authorship rather than being re-authored to
whoever pressed the button.

FP8 resident MoE experts and a SharedK-WMMA prefill path for Gemma-4 on RDNA4
ROCm, plus a CPU seam test that pins the portable vt::ExpertGeGLUFp8* symbols
link and the recipe env knobs parse inertly.

The concern this PR was originally held on is CLOSED BY MEASUREMENT, not by
reading the diff. It was held because ~1,600 added lines sat in gemma4.cpp and
gemma4_moe.cpp and Gemma-4 is a gate model. bakon11 rebuilt it to drop the CUDA
forward restructure entirely, and both arms were then gated on dgx GB10 in one
flock window: `gmain` (main) and `g317` (main + this PR) each returned
gemma4-E4B STRICT 32/32 token-exact vs the vLLM 0.25.0 golden. gemma4.cpp is
byte-identical to main.

Its cuda-fat-build red was never its own: the gencode audit failed on seven
files, every one of them Marlin, while this branch touches ROCm and
gemma4_moe.cpp and no Marlin gencode at all. That was the drift #407 fixed and
the branch predated it -- the same stale-base class that also explained #383 and
#393 tonight.

ACCEPTED WITH A NAMED RESIDUAL, recorded because it is a product decision and
not a gate result. This flips four knobs DEFAULT-ON for ROCm:
VT_GEMMA4_PREFILL_PEER_ACT, VT_ATTN_PREFILL_FLASH_SHAREDK,
VT_ATTN_PREFILL_SHAREDK_WMMA and VT_ROCM_GRAPH. Non-ROCm builds are unaffected --
every new lever is env-gated off or behind moe.enabled / ROCm ExpertGeGLU
dispatch -- and each knob is individually reversible by env. But there is NO AMD
CI runner, so those defaults rest entirely on the contributor's lab evidence
(2x AMD Radeon AI PRO R9700 gfx1201 / RDNA4, ROCm 7.x, gemma-4-26B-A4B-it-fp8;
prefill ~2k @11k, short decode ~45-52 t/s). Feature correctness is unprovable on
GB10 and honestly so: all nine cached Gemma checkpoints there are dense bf16 with
enable_moe_block false, so RunGemma4Moe, EnsureGemma4Fp8* and ExpertGeGLU* are
simply unreachable. The GB10 gate proves NON-REGRESSION, never the feature.

Gate: scripts/agent-preflight.sh rc=0 on the merged tree; check-agent-record,
check-doc-checkpoint, check-public-doc-tables, check-now-current, check-env-doc
and check-commit-trailers all green. CI is queue-blocked (#274), so the
operator's own gate run is the authority, as AGENTS.md prescribes.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants