Skip to content

[DSV4][ROCm] Add contract-bound C4 KV compressor (task 19) - #618

Open
inaniloquentee wants to merge 2 commits into
dsv4-rocmfrom
feat/dsv4-kv-c4-bi
Open

inaniloquentee wants to merge 2 commits into
dsv4-rocmfrom
feat/dsv4-kv-c4-bi

Conversation

@inaniloquentee

Copy link
Copy Markdown
Collaborator

Summary

Implements roadmap item 19, the contract-bound DSV4 main-KV C4 compressor on the dsv4-rocm integration branch.

  • Emits only complete four-token current groups and preserves global request/group identity.
  • Implements the fixed eight-token overlap window: previous-group first half plus current-group second half; the first group uses a masked predecessor window.
  • Supports explicit context-only predecessor groups for microbatch boundaries via emit_group_ids.
  • Enforces CP ownership by the current group?? fourth token and reuses the real aiter paged compressed-cache layout (k_per_block=64).
  • Reuses pinned aiter flydsl_fused_compress_attn with overlap=True, ratio 4, BF16 output and fixed one-wave arithmetic; no new numerical GPU kernel was added.
  • Reuses pinned mLite/Megatron ratio-4 compressed_compact_graph for VJP and RL Kernel CanonicalBackwardSession for fixed-order parameter-row reduction.
  • Keeps live request state/cache commit in item 21 and runtime/prefill/decode/Graph integration in item 27.

Validation on MI300X

Environment: AMD Instinct MI300X VF gfx942, ROCm/HIP 7.14.60850, PyTorch 2.12.0+rocm7.14.0a20260608, Triton 3.7.0, FlyDSL 0.3.4.1, aiter commit 326cffa38c9eda16bef19375ba55334e4d3acd59, mLite commit 327a238239448de6771beea185dc6902a8de3eaa.

  • Full targeted contract + DeepSeek-V4 suite: 221 passed, 0 skipped.
  • CPU-safe C4 tests: 53 passed.
  • Real MI300X tests: 26 passed, including actual aiter overlap/paged cache, mLite VJP, fixed-order overlap gradients, streams, fresh-process/JIT isolation and CP ownership.
  • Scoped Python wrapper coverage: 90% statements, 80/86 branches; native FlyDSL instructions are not measured by Python coverage.
  • Ruff format/lint passes.

Detailed evidence and benchmark: reports/kv-compressor-c4-mi300x-20261028.md.

Explicit non-claims

This PR does not claim RCCL/multi-GPU CP acceptance, Graph replay, live cache commit, prefill/decode framework integration or full-model acceptance. Those remain separate roadmap work.

@coderabbitai

coderabbitai Bot commented Oct 27, 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: f093a70d-bfdd-4101-8ec6-aaefcb71f70f

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 27, 2026 •

Copy link
Copy Markdown

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

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

⚠️ 4 warning(s) occurred during review.


maintainability · medium

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

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

megatron_backward_path is used without any path validation, so a caller-supplied path to an arbitrary Python file that happens to hash to the pinned digest... more importantly, when the env var is set to an empty string, or falls through to the import branch — that is fine — but a directory path, or a path whose digest matches nothing, will surface as source_hash C4Error. That part is OK. The real gap: the loaded module is executed via spec.loader.exec_module and only compressed_compact_graph is read; nothing verifies the attribute is callable, unlike the aiter branch which checks callable(...). If the pinned file ever fails to expose it, the failure surfaces later in vjp as an AttributeError inside autograd instead of a fail-closed C4Error at construction.

Consider require(callable(getattr(module, "compressed_compact_graph", None)), ...) mirrored from the aiter check.


maintainability · medium

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

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

Same construction-time contract gap: no require that the pinned mLite module actually provides a callable compressed_compact_graph. A hash-matching file with a renamed/incompatible entrypoint passes construction and then fails inside vjp (with a non-C4Error exception) mid-backward, when parameters/gradients are already registered in the canonical session.

💡 Suggested Change

Before:

                self.functional = module.compressed_compact_graph

After:

                self.functional = getattr(module, "compressed_compact_graph", None)
                require(callable(self.functional), "pinned mLite module lacks compressed_compact_graph")

maintainability · low

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

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

importlib.metadata.version("flydsl") raises importlib.metadata.PackageNotFoundError and import_module("triton") raises ImportError when the distribution is absent. Both escape as non-C4Error exceptions, unlike the explicitly wrapped C4Error("pinned aiter C4 ABI unavailable") above. Since the flydsl version is part of the provenance fingerprint, a missing distribution should fail closed with a C4Error rather than an opaque metadata error. Affected in the same constructor path — wrap both lookups (and keep the aiter import wrapping style consistent).

💡 Suggested Change

Before:

            "triton": importlib.import_module("triton").__version__,
            "flydsl": importlib.metadata.version("flydsl"),

After:

            "triton": _version_or_error("triton", importlib.import_module("triton").__version__),
            "flydsl": _metadata_version_or_error("flydsl"),

bug · medium

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

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

Type inconsistency with the reference contract used by _C4Autograd.backward in rl_engine/models/deepseek_v4/kv_compressor_c4.py:

for indices, rows in contributions:
    for physical, row in zip(indices, rows, strict=True):
        if physical >= 0:

indices here is bound.context_indices_host[index] (a CPU tuple, per the C4Groups.context_indices_host: tuple[tuple[int, ...], ...] annotation), while c4_helpers.ReferenceC4Backend.vjp appends the GPU tensor bound.context_indices[index]. Both happen to work today only because the caller iterates with zip(...); any consumer expecting a tensor (e.g. indices >= 0 vectorized) will break on the GPU path only. Also note the dx rows are aligned by strict=True with 8 host indices, whereas the group-0 branch produces dx of length 8 via torch.cat((torch.zeros_like(context[:4]), dx)) where context has 8 rows and source has 4 — this is correct only because context[4:] yields exactly 4 rows; a change to the window width silently breaks the alignment. Recommend asserting len(indices) == dx.shape[0] or switching to the tensor form used by the reference backend.


security · 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 security)

The gather uses the Python-level cache_slots_host, but the kernel writes rows according to the device plan/block_tables. Both derive from the same selected/block_order in bind_complete_groups, so they agree today. However, cache is a freshly allocated torch.empty buffer with no zero_, and the block table is filled with -1 for unused block columns (table = torch.full(..., -1, ...)). If the kernel dereferences a -1 block entry for any plan row (e.g. a state-phase read for a request whose block column was not written), it will index outside [0, blocks) and corrupt device memory silently instead of raising. Confirm the provider treats -1 block-table entries as masked, or clamp/validate them before launch; currently the backend performs no check that every non-negative block id in block_table is < blocks.


bug · medium

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

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

For AITer_C4_LEGACY_SHA256 the launch options dict is empty, so quant_mode and k_split_num_waves are left to provider defaults, i.e. the arithmetic contract "fixed NW=1" in C4Contract.arithmetic is not asserted for that ABI. If the legacy provider defaults to a multi-wave split (or a non-none quant mode), forward numerics silently differ from the contract while provenance still reports the fixed-1 path. The ROCm test only asserts kwargs.get("k_split_num_waves", 1) == 1 (defaulting the missing key to 1), so it cannot catch this. Either pin the legacy defaults explicitly to the contract values, or make the legacy hash fail closed.


security · medium

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

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

blocks is derived only from written slots, yet bound.block_table holds -1 sentinels and is sized (len(request_order), max_block_column). Before launch, assert bound.block_table[bound.block_table >= 0].max() < blocks (or size the cache from max_block_column * len(request_order)) so an inconsistent binding fails closed rather than launching a kernel that can address past the allocation.

💡 Suggested Change

Before:

        blocks = max((block for block, _slot in bound.cache_slots_host), default=-1) + 1

After:

        blocks = max((block for block, _slot in bound.cache_slots_host), default=-1) + 1
        require(
            int(bound.block_table.max()) < blocks, "C4 block table exceeds cache allocation"
        )

documentation · medium

📄 benchmarks/benchmark_kv_compressor_c4.py (L57-L62)

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

The provider_forward_us case calls the backend directly and skips the operator's Python-level validation and paged-cache gather, whereas the recorded metric says "Python validation and paged-cache gather included". The two numbers are therefore non-equivalent and reported side-by-side under a metric description that only applies to forward_us, which can mislead conclusions about provider-level cost. Either exclude the provider call's cost difference from the metric description or make the measured work explicit.


maintainability · low

📄 benchmarks/benchmark_kv_compressor_c4.py (L51-L51)

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

output_bytes hardcodes 2 bytes/element while the input size is derived from values.element_size(). If the output dtype ever differs from bf16, the reported output throughput will be silently wrong. Derive it from the contract/output dtype instead of a magic constant.


maintainability · low

📄 benchmarks/benchmark_kv_compressor_c4.py (L52-L55)

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

operator.bind performs CPU-side validation only (per test_invalid_input_never_reaches_backend, no backend call). Timing it with timed()'s CUDA-synchronized wall clock around a GPU-synchronize in each timed batch measures mostly CPU enqueue/py overhead and is not comparable to the GPU-bound forward_us in the same record. Consider timing bind with pure CPU wall-clock over the repeat loop (or label it explicitly as CPU-only).


⚠️ Warnings:

  • rl_engine/contracts/operators/kv_compressor_c4.py,rl_engine/integrations/common/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/contracts/operators/kv_compressor_c4.py (token_budget_reached): stopped group "rl_engine/contracts/operators/kv_compressor_c4.py,rl_engine/integrations/common/kv_compressor_c4.py,rl_engine/models/deepseek_v4/kv_compressor_c4.py" mid-review: used 557697 tokens exceeds budget 500000
  • rl_engine/backends/rocm/attention/kv_compressor_c4.py,rl_engine/integrations/engines/rollout/vllm/kv_compressor_c4.py,rl_engine/integrations/engines/train/megatron/kv_compressor_c4.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round completed the task
  • benchmarks/benchmark_kv_compressor_c4.py,tests/models/deepseek_v4/c4_helpers.py,tests/models/deepseek_v4/c4_process_probe.py,tests/models/deepseek_v4/test_kv_compressor_c4.py,tests/models/deepseek_v4/test_kv_compressor_c4_rocm.py (review_limit_reached): reached the aggregate token budget before finishing; the grace round completed the task

Comment on lines +20 to +28
def timed(fn, repeats):
for _ in range(5):
fn()
torch.cuda.synchronize()
start = time.perf_counter()
for _ in range(repeats):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - start) * 1e6 / repeats

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
This times host wall clock around an async device loop only. Peers in this repo report GPU-event and wall time together (see benchmarks/backends/rocm/benchmark_rocm_singleton_lse.py::_elapsed_ms). Without the event pair, forward_us / provider_forward_us are dominated by Python-side work in the timed region: torch.empty + two zeros_like in AiterC4Backend.forward, the (blocks, 64, 512) BF16 cache allocation, the clone() of context_values, the Python loop over logical_keys, and (for bind_us) a blocking token_ids.cpu().tolist(). Recording kernel time alongside wall time makes it explicit which part is host/launch overhead, so the JSON is not read as kernel latency.

Also, this torch.cuda.synchronize() and the device="cuda" fixtures in main() are unconditional, so the script cannot run (or clearly skip) on a non-CUDA/ROCm host; the repo's guarded pattern is if device.type == "cuda" (benchmark_rocm_logp.py::_device_synchronize).

Suggestion:

Suggested change
def timed(fn, repeats):
for _ in range(5):
fn()
torch.cuda.synchronize()
start = time.perf_counter()
for _ in range(repeats):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - start) * 1e6 / repeats
def timed(fn, repeats):
for _ in range(5):
fn()
torch.cuda.synchronize()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
wall = time.perf_counter()
start.record()
for _ in range(repeats):
fn()
end.record()
end.synchronize()
return (
start.elapsed_time(end) * 1e3 / repeats,
(time.perf_counter() - wall) * 1e6 / repeats,
)

Comment on lines +147 to +153
if group > 0:
grad_out = torch.cat((torch.zeros_like(y[:1]), grad[index : index + 1].float()))
dx, da, dn = torch.autograd.grad(y, (context, a, n), grad_outputs=grad_out)
else:
dx, da, dn = torch.autograd.grad(
y, (source, a, n), grad_outputs=grad[index : index + 1].float()
)

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 group > 0 branch builds grad_out with a leading zero row plus the incoming grad, i.e. it assumes compressed_compact_graph returns two output rows per group (a first row to be masked like the previous-window slot). The group == 0 branch passes grad[index:index+1] (one row) for the truncated source. In the CPU reference (c4_helpers.context_reference) the functional returns a single pooled row and the whole y receives the incoming grad, with the previous-window masking handled by the -inf gate rather than by zeroing a gradient row. If the pinned mLite functional also emits one row per group, grad_out would be length 2 against a length-1 y and torch.autograd.grad raises, or (if it emits 2 rows) the first branch silently drops half of the group output's gradient. The two branches are mutually inconsistent about y's output arity — please verify against the pinned backward.py and make both branches use the same output-row convention.

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

@Flink-ddd Flink-ddd added platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA) DSv4 deepseek-P2 labels Oct 28, 2026
Comment on lines +128 to +129
for index, (_request, group) in enumerate(bound.logical_keys):
context = bound.context_values[index].float().detach().requires_grad_()

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
For group == 0, source = context[4:] is a view of the leaf tensor context, and torch.autograd.grad(y, (source, a, n), ...) returns the gradient with respect to that view's shape (4 rows). That is then padded with torch.zeros_like(context[:4]) to 8 rows. This relies on the predecessor halves being exactly zeroed for group 0 (context[:, :4] is zeroed at bind time). If the predecessor rows were ever non-zero for a group-0 binding, the returned dx would silently drop the predecessor contribution to ape/norm (the predecessor rows' effect on the softmax window) because only source is differentiated there. Cross-checking against c4_helpers.context_reference, group 0 uses torch.zeros_like(previous) for content and -inf gate — i.e. the predecessor rows are constant, so they contribute zero gradient through context but still contribute to da/dn in the reference implementation (which differentiates the full 8-row context). Here the VJP differentiates only 4 rows for group 0, so da/dn for the group-0 window differ from the reference: the reference's ape/norm gradient includes the (zero-content, -inf-gated) predecessor rows' weight dependence, whereas source=context[4:] excludes them by construction. This produces silently different APE/RMS gradients for the first group of every request; it is only masked in tests because group 0 has zeroed content and the accumulated test tolerances are loose (rtol=4e-3). Recommend differentiating the full context for group 0 (masking the same way forward does) and taking dx[4:], matching the reference semantics.

@inaniloquentee

Copy link
Copy Markdown
Collaborator Author

Pushed 879305251c37e1b8bb0cb44409f1762752d03561 after migration to the new MI300X environment.

Correctness fixes: use aiter's native negative-position padding mask instead of the old finite gate floor; validate predecessor context even when not emitted; preserve real FP64 in the independent oracle (the old helper downcast internally, so the previous all-FP64 description was inaccurate).

New environment: Torch 2.11.0+gitd0c8b1f, HIP 7.2.53211, Triton 3.6.0, aiter 0.1.16.post3, FlyDSL 0.2.0. Exact provider source hashes distinguish the admitted ABI; no silent fallback.

Validation: 85 C4 tests passed and 227 full targeted tests passed, zero skips, one warning. Lint/format passed; benchmark rerun. Details and limitations: reports/kv-compressor-c4-new-host-20261029.md.

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

deepseek-P2 DSv4 platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants