Skip to content

feat(dsv4): BI grouped output projection with native aiter VJPs (item 23) - #627

Open
inaniloquentee wants to merge 25 commits into
dsv4-rocmfrom
feat/dsv4-o-proj-grouped-bi
Open

inaniloquentee wants to merge 25 commits into
dsv4-rocmfrom
feat/dsv4-o-proj-grouped-bi

Conversation

@inaniloquentee

Copy link
Copy Markdown
Collaborator

Roadmap item 23 ??grouped attention output projection

Target: dsv4-rocm. Task-only commit: 807e58b5434f302e4fd3d587a0e02660b6e6b2bc.

Explicit prerequisites / review base

  • Reuses item 13 ([DSV4][ROCm] Add contract-bound BI Indexer projections #590, f9c8b27d) rather than duplicating its native BF16/FP8 GEMMs, vLLM quantization/checkpoint ABI, and canonical VJP plumbing.
  • Includes current main 8588158650aff58c9c1f527674de94bad06c3d10, merged as 1becef6d. The target branch predates that main. Review item-23 changes against 1becef6d; the PR diff against dsv4-rocm also exposes those prerequisites.

Implementation

  • Fixed order: inverse RoPE -> global eight-head groups -> wo_a -> ordered concat -> wo_b.
  • Native aiter cached RoPE and batched BF16 GEMM; explicit per-group native FP8 profile via the shared item-13 backend; no new numerical kernels or silent math fallback.
  • BF16-master autograd: native adjoint/dX and canonical logically sorted parameter gradients. Quantized-byte derivatives are explicitly not claimed.
  • Typed checkpoint/version/head/row contracts, owned metadata, mutation barriers, FP32 TP partials with fixed-rank local merge and opt-in vLLM/Megatron bindings.
  • Provider identity includes native RoPE binaries, upstream sources and shared bridge/session sources.

Tests and evidence

MI300X/gfx942; Torch 2.11, HIP 7.2, Triton 3.6, aiter 0.1.16.post3.

  • 316 passed, zero skips, twice in fresh processes after updating to current main.
  • 110 new tests: 63 CPU contract/oracle tests, 47 real ROCm tests; 206 prerequisite regressions.
  • Tests numerical FP64 oracles separately from exact-byte chunk/permutation/backward-order/train-inference invariance; FP8 checkpoint/activation and master-VJP checks; empty/padded/tail/large geometries; local TP1/2/4 output and gradient assembly.
  • New production statement coverage 311/316 (98.42%); branches 56/60 (93.33%). Native instructions are not included.
  • Ruff checks pass. Benchmarks, provider/source hashes and limitations are committed under reports/o-projection-mi300x-20261029.{md,json}.

Scope boundaries (not hidden as passing tests)

Graph is explicitly rejected. Local TP assembly is not actual RCCL transport or invariance across parallel degrees. No claim of full-model vLLM/Megatron/verl/vime integration, shuffled checkpoint support, CP/DCP, LoRA, or FP8 throughput speedup. These remain independent roadmap work.

fangfangssj and others added 25 commits October 24, 2026 08:44
Signed-off-by: fangfangssj <1135470306@qq.com>
Signed-off-by: Ziyi <ziyizhao0723@gmail.com>
ws1-gtest-gpu and ws1-chain-gpu fail at "Configure runpodctl" before any
test runs. They install releases/latest (v2.14.0 since 2026-09-10), whose
deprecated `config --apiKey` aborts with
`Config File ".runpod.yaml" Not Found` when it is the first runpodctl
call on a fresh HOME. It only succeeds once an earlier call has created
~/.runpod/config.toml, which is why gpu-ci, whose install step happens to
run `runpodctl version` first, got past the same command.

Drop the config step in all three workflows. runpodctl v2 reads
RUNPOD_API_KEY from the environment, and every step that runs
ci/run_gpu_ci.sh already exports it, including the cleanup trap that
calls `pod remove`. Verified for v2.9.0 inside `unshare -rn`, from a
fresh HOME after `runpodctl version`: without the variable `pod list`
fails locally with no_credentials, with RUNPOD_API_KEY=dummy it gets
past the credential check to a network_error, and config.toml keeps
`apikey = ''` throughout. v2.14.0 behaves the same.

Pin the download to v2.9.0, the version gpu-ci run 31455127310 exercised
end to end (pod create with sold-out fallback, pod id parsing, pod get
polling, SSH, cleanup), and verify it with sha256sum -c against the
runpodctl-linux-amd64 line of checksums_2.9.0_sha256.txt
(06e6f54957db79d5cd9f1909a7f1d365076826751ba2f5df65d75dde43a64148),
which matches GitHub's asset digest.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
fix(ci): pin runpodctl v2.9.0 and drop the deprecated config step (RUNPOD_API_KEY auth)
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
ci: remove obsolete RunPod WS1-chain-GPU workflow
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
ci: add RL-Kernel-specific automatic OCR review
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
Signed-off-by: lamentropetion <3051000145@qq.com>
docs(rocm): make the vLLM companion base reproducible from upstream
[Ascend] Fix deterministic attention backward compilation
@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 774ff724-c848-469e-b36d-cb1e64acb880

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 15 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)
  • 📋 Routed to summary by policy: 10 comment(s)

⚠️ 4 warning(s) occurred during review.


security · medium

📄 .github/workflows/ocr-review.yml (L76-L78)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity medium · category security)

This pool-of-triggers job runs on ubuntu-latest with pull-requests: write and an LLM credential, and relies entirely on the action's own default-branch checkout for safety. Two hardening gaps versus the documented trust boundary: (1) the job has no explicit permissions: contents: read scoping at the job level, so any step added later inherits the broader token; (2) if vars.OCR_LLM_URL/OCR_LLM_MODEL is attacker-controllable in a fork context, the secret token is still sent to that URL. Consider (a) narrowing permissions per workflow/job (the reminder step is the only one needing pull-requests: write — split it into a separate job), and (b) allowlisting the URL from a secret-only source rather than a repository variable. Note the last step that posts a comment is fine, but the review step itself only needs contents: read.


security · medium

📄 .github/workflows/ocr-review.yml (L84-L85)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity medium · category security)

vars.OCR_LLM_EXTRA_BODY is interpolated verbatim into a privileged step's inputs from a repository variable, which any user with repo-variable write access (including some maintainers of lower tiers) can change without review. Since it is provider request-body JSON, an attacker or a typo can redirect the request (e.g. override the URL/model) and cause the OCR auth token to be sent elsewhere. Consider treating it as a secret or reading it from a checked-in trusted file under .opencodereview/ (already CODEOWNERS-gated) rather than a mutable variable.


maintainability · low

📄 .github/workflows/gpu-ci.yml (L54-L55)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

wget -qO without --fail/--show-error will happily save an HTML 404/502 page to runpodctl and only fail later at sha256sum -c - with a mismatched-hash message. That makes a broken/renamed release URL indistinguishable from a corrupt download. Use wget -q --fail --show-error (or curl -fsSL) so HTTP errors are reported accurately, and keep the checksum as defence-in-depth.

💡 Suggested Change

Before:

          wget -qO runpodctl "https://github.com/runpod/runpodctl/releases/download/${RUNPODCTL_VERSION}/runpodctl-linux-amd64"
          echo "${RUNPODCTL_SHA256}  runpodctl" | sha256sum -c -

After:

          wget -q --fail --show-error -O runpodctl "https://github.com/runpod/runpodctl/releases/download/${RUNPODCTL_VERSION}/runpodctl-linux-amd64"
          echo "${RUNPODCTL_SHA256}  runpodctl" | sha256sum -c -

maintainability · low

📄 .github/workflows/ws1-gtest-gpu.yml (L100-L101)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

Same as gpu-ci.yml: add --fail --show-error to wget so an HTTP error from the release URL is not persisted as a bogus binary and mis-reported as a checksum mismatch.

💡 Suggested Change

Before:

          wget -qO runpodctl "https://github.com/runpod/runpodctl/releases/download/${RUNPODCTL_VERSION}/runpodctl-linux-amd64"
          echo "${RUNPODCTL_SHA256}  runpodctl" | sha256sum -c -

After:

          wget -q --fail --show-error -O runpodctl "https://github.com/runpod/runpodctl/releases/download/${RUNPODCTL_VERSION}/runpodctl-linux-amd64"
          echo "${RUNPODCTL_SHA256}  runpodctl" | sha256sum -c -

bug · medium

📄 rl_engine/models/deepseek_v4/o_projection.py (L215-L220)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity medium · category bug)

In _GroupedLinear.backward, parts[0] is also evaluated when ctx.slots is empty (e.g. all rows padded, so _register_canonical returned None — see the if not logical_rows.active_keys: return None path; but here x.shape[0] > 0 so the tuple is non-empty only if logical.active_keys is non-empty). More concretely: parts is built over ctx.slots, and dw = torch.stack(parts) returns a grad of shape [G,R,K] — correct only if every slot submitted. But if the whole batch is padding, _GroupedLinear.forward still registered slots returning None, so parts is all zeros_like and dw is a full zero grad rather than None; combined with session.validate_complete() (which will flag the un-submitted slots) this is at least inconsistent. Guard parts emptiness before indexing it: if parts[0] is not None raises IndexError when x.shape[0] == 0 reaches backward with no slots.


bug · medium

📄 rl_engine/backends/rocm/gemm/o_projection.py (L67-L70)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity medium · category bug)

AiterOProjectionBackend.grouped early-returns torch.empty((x.shape[0], 0, weight.shape[1])) when x.shape[1] == 0, but the same empty-input path is not handled consistently with inverse_rope: an x with shape[0] == 0 produces an empty tensor through self._grouped, and _GroupedLinear.forward/backward then rely on torch.stack of per-group results. More importantly the empty-guard condition is on shape[1] (K=group_width) whereas the documented empty case is M == 0; if weight.shape[1] == 0 the returned dtype/device is hardcoded bfloat16 while the contract demands the same dtype as the non-empty path. Please also guard x.shape[0] == 0 explicitly and derive dtype from the input rather than hardcoding.


documentation · low

📄 rl_engine/contracts/operators/o_projection.py (L102-L106)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category documentation)

family_fingerprint strips tp_rank but keeps tp_size, and it is the identity used by merge_tp_partials to prove all ranks share arithmetic. This is fine, but fingerprint (used for weight/row version barriers) includes tp_rank, so weights prepared for rank 0 cannot be validated by rows bound for rank 1 — which is intended. No defect; noting only that the docstring in merge_tp_partials should state this. If a future contract adds TP-dependent geometry, family_fingerprint will no longer be TP-invariant. Not blocking.


bug · medium

📄 rl_engine/models/deepseek_v4/indexer_projection.py (L197-L199)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity medium · category bug)

When packed is not None, weight saved in forward is the BF16 training master, but if _register_canonical returned None (e.g. logical_rows.active_keys empty), dw is silently set to zeros while no canonical contribution is recorded for the parameter. Combined with the fact that the forward path for packed is FP8, a trainable master receives a zero gradient instead of an error. Consider raising when weight.requires_grad and slot is None, since that combination is not a legitimate "no rows" case.


performance · low

📄 rl_engine/models/deepseek_v4/indexer_projection.py (L116-L117)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category performance)

torch.isfinite(data.float()).all() above upcasts the entire packed FP8 matrix to float32 (4x the packed size) and forces a device drain; for the largest Q matrices (q_width x q_lora_rank = 8192x1024 or larger) this is a measurable memory spike at preparation time. Consider checking finiteness on structured slices or, if the loader already validates, documenting that this validation is intentionally eager.


other · low

📄 rl_engine/contracts/operators/indexer_projection.py (L42-L43)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category other)

self.q_width is a property defined below; it is accessible here, but note that q_width depends on num_heads/head_dim which were validated earlier in the loop, so this is safe. No change required — flagging only that the 128-alignment check for q_lora_rank % 128 (K dimension) is consistent with the backend's BLOCK_SIZE_K=128 and scale [N/128, K/128] layout.


⚠️ Warnings:

  • .github/CODEOWNERS (token_budget_reached): skipped round 2 of group ".github/CODEOWNERS,.github/workflows/gpu-ci.yml,.github/workflows/ocr-review.yml,.github/workflows/ws1-gtest-gpu.yml,.opencodereview/rule.json": used 556096 tokens exceeds budget 500000
  • benchmarks/benchmark_indexer_projection.py (token_budget_reached): stopped dispatch: used 556096 tokens + group estimate 109732 = projected 665828 exceeds budget 500000
  • rl_engine/backends/rocm/gemm/o_projection.py,rl_engine/contracts/operators/o_projection.py,rl_engine/integrations/common/o_projection.py,rl_engine/integrations/engines/rollout/vllm/o_projection.py,rl_engine/integrations/engines/train/megatron/o_projection.py,rl_engine/models/deepseek_v4/o_projection.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round did not complete the task
  • rl_engine/backends/rocm/gemm/indexer_projection.py,rl_engine/contracts/operators/indexer_projection.py,rl_engine/integrations/common/indexer_projection.py,rl_engine/integrations/engines/rollout/vllm/indexer_projection.py,rl_engine/integrations/engines/train/megatron/indexer_projection.py,rl_engine/models/deepseek_v4/indexer_projection.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round did not complete the task

- name: Configure runpodctl
run: runpodctl config --apiKey "${{ secrets.RUNPOD_API_KEY }}"

- name: Setup SSH key

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · critical
The Configure runpodctl step (runpodctl config --apiKey "${{ secrets.RUNPOD_API_KEY }}") was removed, but ci/scripts/run_gpu_ci.sh still runs runpodctl pod create/get/remove (lines 36/50/67/98) and never reads RUNPOD_API_KEY. With no ~/.runpod/config.toml written, every runpodctl call is unauthenticated: pod create fails, POD_ID stays empty, and the cleanup trap silently skips teardown — the GPU CI job cannot provision pods (and any partially created pod leaks). Restore the configuration step (or configure from the env var before invoking the script).

run: runpodctl config --apiKey "${{ secrets.RUNPOD_API_KEY }}"
runpodctl version

- name: Setup SSH key

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · critical
Same regression as gpu-ci.yml: removing runpodctl config --apiKey leaves run_gpu_ci.sh's runpodctl pod create/get/remove calls unauthenticated (RUNPOD_API_KEY in the job env is never read by the script). The WS1 GPU gate will fail at pod creation with no cleanup. Restore the configuration step.

Comment on lines +131 to +137
def weight_gradient(self, rows, grads):
return self.linear(
grads.t().contiguous(),
rows.t(),
output_dtype=torch.bfloat16,
role="dw",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · high
rows.t() is passed as the weight operand to linear, which calls self.plan(x.shape[1], weight.shape[0], role). For DW the expected output is [K, N] = [rows.shape[1], grads.shape[1]]. Here x = grads.t().contiguous() has shape [N, M] so x.shape[1] == M, and weight.shape[0] == rows.shape[1] == K. The plan is therefore built for k=M, n=K (the reduction/shape pair is swapped) — the frozen tile/SPLITK config is chosen for the wrong geometry, and more importantly the underlying GEMM computes grads.t() @ rows.t() = [N, K] rather than the required [K, N] DW (or an [K, N] result requires rows.t() @ grads.t() order). Verify against the canonical_backward submit_linear contract: the operand order and plan dimensions here look transposed relative to a [K,N] weight-gradient output.

Comment on lines +181 to +188
if weight.requires_grad:
ctx.session = active_session()
require(ctx.session is not None, "trainable weights require canonical_backward_session")
ctx.slots = tuple(
_register_canonical(ctx.session, f"{prefix}.{g}", weight, logical)
for g in range(x.shape[0])
)
if packed:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · critical
_GroupedLinear.forward registers a canonical slot per group whenever weight.requires_grad, but the packed branch then never submits to those slots (it calls backend.linear_fp8 directly). With a_precision="block_fp8_ue8m0" plus a trainable BF16 master (the documented BF16-master-surrogate VJP profile) every registered slot stays pending: session.validate_complete() raises "incomplete canonical submissions", and if A also has requires_grad, dw is never produced. The forward must either take the canonical path (surrogate master GEMM) when slots were registered, or refuse registrations in the packed branch.

Comment on lines +407 to +413
elif weights.packed_a:
z = torch.stack(
[
backend.linear_fp8(grouped[g], p, output_dtype=torch.bfloat16)
for g, p in enumerate(weights.packed_a)
]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · high
In OProjection.__call__, when a_grad is False but weights.packed_a is non-empty (FP8 A checkpoint with frozen weights — the normal inference case), the in-place torch.stack([backend.linear_fp8(...)]) branch runs without any _GroupedLinear/canonical registration. That is fine for grad, but the same branch is also reachable when a_grad is True? No — a_grad requires the BF16 master and then _GroupedLinear.apply is used. However, the opposite combination is unchecked: a_precision == "block_fp8_ue8m0" with a trainable master goes through _GroupedLinear, whose forward takes the packed branch and never records the canonical slot in the actual computation, see the comment on _GroupedLinear.forward. Please make the two dispatch sites agree (and assert here that a trainable A always has packed_a == () or vice-versa) so grads cannot be silently dropped.

@github-actions

Copy link
Copy Markdown

@inaniloquentee The automated OCR review has completed.

Please review and address all automated review bot findings, then push your updates. Maintainers will begin manual review after the bot findings have been addressed.

This branch has not been deployed

No deployments
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.

6 participants