Skip to content

[DSV4][ROCm] Add versioned compressor state and single cache commit (task 21) - #625

Open
inaniloquentee wants to merge 3 commits into
dsv4-rocmfrom
feat/dsv4-compressor-state-bi
Open

inaniloquentee wants to merge 3 commits into
dsv4-rocmfrom
feat/dsv4-compressor-state-bi

Conversation

@inaniloquentee

Copy link
Copy Markdown
Collaborator

Roadmap task 21 ??compressor state and single cache commit

Dependency: stacked on task 19 / #618 at 8793052. Target remains dsv4-rocm. The task-21-only commit is 49dc1795f3516172dbd125e6b6dfcaa25f9d1ac9; the base diff includes the prerequisite until #618 merges. Refs #512.

Reuse

  • Existing aiter C4/C128 compression, RMSNorm, RoPE and paged cache scatter.
  • Existing vLLM save_partial_states for FP32 [kv,score+ape] history, strictly after compression.
  • Preallocated layout adapters and real Torch Graph replay; no new numerical GPU kernel.

Lifecycle

Request generations, model/position versions, explicit unique writer, owned staging leases, exactly-once commit, incomplete-group handling, immutable snapshots, release/page reuse and model replacement. Duplicate/stale/foreign transactions and capacity/ownership errors fail before launch. GPU write failures poison the arena instead of silently retrying a partial commit. Device completion precedes cursor publication, avoiding early staging or page reuse across threads/streams.

Explicit C128 profile boundary

The required arithmetic handshake admits the fixed single-wave online-FP32 C128 state profile. It does not silently substitute for task 18's eight-wave HCA profile. Mismatched recipes are rejected. Full training/rollout equivalence across those different profiles is not claimed.

Real MI300X acceptance

  • 316 tests passed twice, zero failures/errors/skips: 54.58s with coverage; 39.50s repeat.
  • 39 new CPU + 50 real GPU tests, 85 C4 regressions, 142 existing contract regressions.
  • Full/chunked/decode/Graph cache and FP32 state bytes, multiple seeds, mixed requests, cross-page reuse, warmup non-mutation, versions and model replacement.
  • Unique ring writers, duplicate lease rejection, stale handles, input alias reuse, output snapshots, thread race, cross-stream handoff, stable Graph buffers and 40 allocation-stable release/recreate cycles.
  • Genuine FP64 numerical reference and fault injection.
  • Python statement coverage 316/320 (98.75%), branches 27/28 (96.43%); no native instruction coverage claim.
  • Lint/format pass; 30-repetition synchronized append benchmarks included.

Documentation: docs/models/deepseek_v4/compressor-state.md.
Evidence/report: reports/compressor-state-mi300x-20261029.md.

No actual distributed CP/RCCL, multi-node, full framework registration, full-model throughput, cache-write autograd or eight-wave C128 equivalence is claimed. Commit is intentionally synchronous; overlap is separate work.

@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: 060c555d-5aa2-4509-96c7-2ab05a957717

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 10 issue(s) in this PR.

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

⚠️ 6 warning(s) occurred during review.


bug · medium

📄 rl_engine/contracts/operators/compressor_state.py (L62-L67)

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

The packed-state term double counts _state: the cache manager allocates _state as (max_requests, state_rows, 2*width) FP32 and two separate readers _read_kv/_read_score of state_rows*width FP32, so the required bytes are max_requests*state_rows*width*(2+1+1)*4. The written term state_rows*width*4*4 equals that only because each request is charged 4 widths; combined with plan_rows*16 (the actual plan is plan_rows*4 int32 = 16 bytes/row here, fine) the budget happens to match for the default fields, but the expression does not describe the allocation and will drift if the reader representation changes. Additionally the per-step temporaries created in prepare are omitted entirely: copy = values.detach().clone() is a full BF16 step buffer and copy.float() allocates a full FP32 step buffer (max_tokens_per_step*2*width*4 = 2 MiB at defaults, i.e. 2x the declared staging term max_tokens_per_step*(width*4+16)). A contract can therefore pass require(self.resident_bytes <= self.max_arena_bytes) while the real per-step device footprint is ~2 MiB above the declared budget, and for small max_arena_bytes values the cap does not bound the actual peak. Make each term reflect an actual allocation and include the FP32 validation temporary (or avoid materializing it, e.g. validate via a chunked/torch.isfinite(copy) route).


bug · medium

📄 rl_engine/models/deepseek_v4/compressor_state.py (L329-L329)

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

_capture() asserts (self._state == 0).all() after warmup/capture (line 225-228), and test_capture_warmup_does_not_commit_or_write_partial_state relies on it. That invariant holds only because warmup runs before any create_request, so _write_slots/_block_tables are default and no valid plan exists. However _capture is invoked from __init__ after _state/_cache allocation, so this is fine today, but note the assertion also validates that the pad plans never write. Separately, first_writer clamps to 0 whenever count <= state_rows, so for repeated small steps (e.g. decode with count=1 at start_position=5) the single writer slot is start_position % state_rows, which differs from the slot (start_position+1) % state_rows written by the previous step — the state ring is therefore advanced correctly, but _write_slots for a short step overwrites a ring slot whose previous value may still be needed by a pending compression window (windows read state_rows history entries). Please confirm the ring-slot reservation when count < state_rows and start_position is mid-group; if the intent is that only the newest state_rows writers are kept, the max(0, count - state_rows) clamp is a no-op for short steps and provides no deduplication.


bug · low

📄 rl_engine/backends/rocm/attention/compressor_state.py (L45-L46)

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

These importlib.metadata.version(...) lookups sit outside the try/except (ImportError, AttributeError) block, so a missing flydsl (or triton) distribution raises importlib.metadata.PackageNotFoundError (a ValueError subclass) instead of the intended C4Error("required aiter/vLLM state ABI unavailable"). This surfaces as an unhandled exception to a caller that only catches C4Error, breaking the backend-availability contract. Wrap the metadata lookups in the same try/except (catching importlib.metadata.PackageNotFoundError) and raise C4Error.


bug · medium

📄 rl_engine/backends/rocm/attention/kv_compressor_c4.py (L88-L88)

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

C4Groups.cache_slots_host is derived from the model-side constant group // 64 block geometry while the cache buffer here is sized (blocks, 64, 512) with a literal slot width of 64. max_block_column in bind_complete_groups uses the same 64, so this pair is consistent today, but the literals are duplicated across layers with no assertion tying them to k_per_block. If either side changes (e.g. a contract profile with a different block size), the returned cache[block, slot] gather and the kernel's block indexing silently diverge, producing wrong rows or out-of-range slot access. Derive the slot count from a shared constant/contract field instead of a literal 64.


bug · medium

📄 rl_engine/models/deepseek_v4/kv_compressor_c4.py (L290-L290)

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

register_parameters stores a mutable set inside the tuple kept in session._c4_bindings and mutates it in place (entry[2].update(...)), while the duplicate-group check entry[2].intersection(bound.logical_keys) runs against that same accumulating set. The check therefore compares a microbatch's keys against all previously registered keys for that parameter id, which is the intended dedupe, but the state is kept on an ad-hoc private attribute and is never cleared by the session itself. Note also that session.register(...) is called in the second loop even when the first loop already found and validated an existing entry, so a second forward for the same layer bumps uses[parameter_id] to a new slot; submit_rows later requires received == len(entries), so the count still lines up only because each call registers exactly one slot per parameter. If a layer reuses the same parameter_id with a different bound.version the identity check fires (good), but there is no reset path for a session that is reused after backward_started; a recycled session would keep stale keys and spuriously raise "duplicate C4 group across microbatches". Document/enforce single-use sessions or reset _c4_bindings when a session is created.


performance · medium

📄 rl_engine/backends/rocm/attention/kv_compressor_c4.py (L119-L122)

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

The output rows are gathered with a Python loop of per-group scalar index operations cache[block, slot] followed by torch.stack. Each element is a separate device kernel launch driven by host-side cache_slots_host, so cost scales with the number of selected groups (bounded only by max_groups = 8192) and serializes against the single fused kernel launch. A vectorized gather built from cache_slots_host (index arithmetic against a flattened view, or a precomputed index tensor passed with the plan) would keep this to one kernel. This is a throughput concern for large group counts, not a correctness defect.


maintainability · medium

📄 rl_engine/backends/rocm/attention/kv_compressor_c4.py (L109-L113)

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

forward hardcodes ratio=4, head_dim=512, rope_head_dim=64, k_per_block=64, preshuffle=True, use_ue8m0=True and overlap=True instead of deriving them from contract, so the launch geometry can silently drift from the validated contract. Because contract.overlap exists as a validated field (== 8) but overlap is passed as a boolean True, the meaning of the two is not obviously aligned; confirm overlap=True corresponds to the contract's fixed 8-token window and consider asserting the invariant here so a future contract profile cannot reach this launch with mismatched geometry.


maintainability · medium

📄 benchmarks/benchmark_kv_compressor_c4.py (L49-L51)

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

Hardcoded output sizing: groups * 4 tokens and groups * 512 * 2 output bytes assume a specific tokens-per-group and output width that are not derived from the fixture/contract. If the fixture shape or contract output dimension changes, the reported tokens/output_bytes become wrong, invalidating any bandwidth/throughput conclusion drawn from the report. Derive these from the fixture (values.shape) and the contract's output shape instead of literals.


⚠️ Warnings:

  • rl_engine/backends/rocm/attention/compressor_state.py,rl_engine/contracts/operators/compressor_state.py,rl_engine/integrations/common/compressor_state.py,rl_engine/integrations/engines/rollout/vllm/compressor_state.py,rl_engine/integrations/engines/train/megatron/compressor_state.py,rl_engine/models/deepseek_v4/compressor_state.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round did not complete the task
  • rl_engine/backends/rocm/attention/kv_compressor_c4.py,rl_engine/contracts/operators/kv_compressor_c4.py,rl_engine/integrations/common/kv_compressor_c4.py,rl_engine/integrations/engines/rollout/vllm/kv_compressor_c4.py,rl_engine/integrations/engines/train/megatron/kv_compressor_c4.py,rl_engine/models/deepseek_v4/kv_compressor_c4.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round did not complete the task
  • rl_engine/backends/rocm/attention/compressor_state.py (token_budget_reached): stopped group "rl_engine/backends/rocm/attention/compressor_state.py,rl_engine/contracts/operators/compressor_state.py,rl_engine/integrations/common/compressor_state.py,rl_engine/integrations/engines/rollout/vllm/compressor_state.py,rl_engine/integrations/engines/train/megatron/compressor_state.py,rl_engine/models/deepseek_v4/compressor_state.py" mid-review: used 651863 tokens exceeds budget 500000
  • tests/models/deepseek_v4/compressor_state_helpers.py (token_budget_reached): stopped dispatch: used 651863 tokens + group estimate 132292 = projected 784155 exceeds budget 500000
  • benchmarks/benchmark_compressor_state.py,benchmarks/benchmark_kv_compressor_c4.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round did not complete the task
  • benchmarks/benchmark_kv_compressor_c4.py (comment_refiled): comment filed against benchmarks/benchmark_compressor_state.py describes code in benchmarks/benchmark_kv_compressor_c4.py; re-filed

)
except (ImportError, AttributeError) as exc:
raise C4Error("pinned aiter C4 ABI unavailable") from exc
provider_hash = source_hash(getattr(self.aiter, "__file__", None))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security · high
The pinned hash is computed from self.aiter.__file__, i.e. the package __init__.py of aiter.ops.flydsl.kernels.fused_compress_attn, not from the module that actually defines flydsl_fused_compress_attn. If the upstream layout defines the entrypoint in a sibling submodule, the pinned SHA256 either never matches (all C4 usage fails at construction) or was computed against the wrong file, which voids the ABI-pin guarantee this check is meant to provide. Pin the hash to the defining module (e.g. sys.modules[self.aiter.flydsl_fused_compress_attn.__module__].__file__) or fail closed when the entrypoint's module differs from the imported package.

Comment on lines +472 to +473
max_position = max(group * 4 for _request, group in bound.logical_keys)
require(max_position < cos.shape[0], "RoPE table misses a global C4 position")

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
The RoPE bound is checked against group * 4, the group's first token, but the kernel reads positions group*4 .. group*4+3 for the current window. A table sized exactly to max_position + 1 (i.e. cos.shape[0] == max(group*4)+1) passes this check while the last three token positions of the last group read past the end of the table. Use max(group * 4 + 3 ...) (and account for the previous-window positions (group-1)*4 for group > 0) so the bound matches the actual in-kernel position range.

@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.

1 participant