Skip to content

Gemma-4 text generation throws on every CUDA build: shared forward calls the ROCm-only vt::GeluMulSeparate (SACRED gate RED on main since 0c2827c1) #377

Description

@localai-bot

Summary

Gemma-4 text generation throws on any CUDA build at the first layer with per-layer embeddings. The SACRED gate test_gemma4_paged_engine is RED on current main:

test case THREW exception: vt::GeluMulSeparate: ROCm-only fast path in this build

This is not a test-harness problem. The same call site is on the production forward, so examples/vllm-server with a Gemma-4 checkpoint on CUDA hits it too.

Mechanism

src/vllm/model_executor/models/gemma4.cpp:652 — the shared Gemma-4 forward, not a ROCm branch — calls vt::GeluMulSeparate unconditionally in the ple > 0 (per-layer-embedding) path:

vt::GeluMulSeparate(d.q, gated.ptr(), gate_lin.ptr(), ple_l.ptr(), T * ple, DType::kBF16);

vt::GeluMulSeparate (src/vt/fused_ops.cpp:56-71) has exactly one implementation:

#if defined(VLLM_CPP_HIP)
  if (q.device.type == DeviceType::kROCM) { rocm::GeluMulSeparateRocm(...); return; }
#endif
  throw std::runtime_error("vt::GeluMulSeparate: ROCm-only fast path in this build");

There is no CUDA, CPU, Metal or Vulkan path, and no build flag can supply one — the fallback is the throw. Gemma-4 E4B has per-layer embeddings, so ple > 0 and the very first layer aborts.

When it broke

0c2827c1perf(gemma4): dual-GPU FP8 resident without host OOM + peer mix (#154), 2026-08-08, on main.

That commit replaced a portable op with the ROCm-only one in the same change that introduced the throw:

-      vt::GeluAndMul(d.q, gated.t(), gate_in.t());  // gelu_tanh(gate_lin)*ple_l
+      vt::GeluMulSeparate(d.q, gated.ptr(), gate_lin.ptr(), ple_l.ptr(), T * ple, DType::kBF16);

git log -S"vt::GeluMulSeparate" -- src/vllm/model_executor/models/gemma4.cpp and git log -S"ROCm-only fast path in this build" -- src/vt/fused_ops.cpp both return that single commit.

Evidence

Two CUDA builds on dgx (GB10, nvcc 13.0, Release, -DVLLM_CPP_CUTLASS_DIR=$HOME/cutlass-4.5.0), run back to back under one flock $HOME/gpu.lock:

arm tree result
baseline main @ 87308dea THREW ... ROCm-only fast path in this build
with PR #317 main + #317 @ a5bd7ad0 identical throw

Identical failure on both arms, so this is not attributable to #317 — it was found while hardware-checking that PR. gemma4.cpp.o compiles byte-identical between the two arms (cmp), which is what makes the attribution airtight.

Why it went unnoticed

test_gemma4_paged_engine is checkpoint-gated and dgx-only: on CI the snapshot is absent, so it emits a SKIP and returns green. The gate can only fail where nobody runs it routinely.

Suggested fix

Give vt::GeluMulSeparate a portable fallback instead of a throw — the semantics are out[i] = gelu_tanh(gate[i]) * up[i], which composes from the existing vt::GeluTanh plus an elementwise multiply, at the cost of one temporary. The ROCm fused kernel stays the fast path where it exists. Reverting the call site to vt::GeluAndMul over a [T, 2*ple] buffer would also work but gives back the ROCm win.

Worth a check on the rest of src/vt/fused_ops.cpp in the same pass: vt::DualRmsNormPlusRes throws "CPU compose not yet wired" on the same principle, so this may not be the only shared call site reachable on a non-ROCm build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions