Skip to content

[WS2][PR4][Logp] feat: add distributed TP/CP logprob drift report - #289

Open
hihaluemen wants to merge 28 commits into
RL-Align:testfrom
hihaluemen:feat/ws2-logprob-distributed-report-pr4
Open

[WS2][PR4][Logp] feat: add distributed TP/CP logprob drift report#289
hihaluemen wants to merge 28 commits into
RL-Align:testfrom
hihaluemen:feat/ws2-logprob-distributed-report-pr4

Conversation

@hihaluemen

@hihaluemen hihaluemen commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Add the cross-config distributed logprob drift runner requested by PR4 of #241.

The runner materializes one explicit TP/CP topology per torchrun invocation, executes the deterministic vocab-parallel logprob reference introduced by PR3, and compares its BF16 selected logprob and vocabulary LSE against an independent full-vocabulary FP32 PyTorch oracle. It emits per-rank and aggregate artifacts with topology, backend, reduction, tolerance, and worst-token provenance.

The distributed logprob implementation itself remains owned by PR3. This PR adds the scoped integration, validation, and reporting layer.

Implements PR4 of #241.

Dependencies and current branch state

This PR depends on the three preceding issue #241 changes:

All three prerequisite PRs are still open. To start PR4 validation without waiting for them to merge, this branch was built on a temporary integration of their current reviewed heads. As a result, a PR opened against the current main will temporarily show prerequisite changes in addition to the PR4 changes.

The PR can be reviewed as a draft while those dependencies are open. The PR4-specific commits are:

f36a63d feat(ws2): add distributed logprob drift runner
1d9bac1 ci(ws2): run logprob comparison tests
f6b5a07 fix(ws2): harden distributed drift reporting
8a4f4be fix(ws2): clean up process groups on setup failure

After #259, #262, and #265 merge, these PR4 changes will be replayed onto the latest main and the branch will be updated so that the final merge diff contains PR4 only. This PR should not merge before its prerequisites.

Known PR3 follow-ups

Review of the stacked PR3 content identified two items that should be resolved in #265 before this PR is rebased for merge:

  • Rank-local validation can currently raise before peer TP ranks enter the next collective. PR3 should exchange local validation status on a trusted TP group and make every rank fail together; the explicit process-group timeout added by PR4 is only a bounded-failure backstop, not a replacement for collective-safe validation.
  • test_matches_ws1_batch_invariant_logp_within_contract_tolerance contains an immediately overwritten contract assignment that should be removed.

The operator documentation in this branch also qualifies the cross-TP bitwise guarantee with its required fixed-tile and tile-aligned-shard conditions. PR4 will sync the final #265 resolution before leaving draft state.

Scope

This PR covers the cross-config integration and distributed drift report described in PR4:

  • Materialize the scoped TP=1/2/4 x CP=1/2 x BF16 topology matrix.
  • Partition the vocabulary across TP ranks and token rows across CP ranks.
  • Execute the explicit pytorch-vocab-parallel-logp-ws2 backend from PR3.
  • Compare distributed BF16 LSE and selected logprob against a full-vocabulary FP32 oracle.
  • Report LSE drift over all logical token rows and dlogp drift over active response tokens only.
  • Verify that replicated outputs are bitwise identical within each TP group.
  • Record per-rank and aggregate topology, shard, backend, reduction, tolerance, and worst-token provenance.
  • Reject implicit backend selection, fallback, topology mismatch, and invalid vocabulary-tile materialization.
  • Record the exact torchrun command required to reproduce every artifact.

CP is represented in topology and report reconstruction, but it is not a numerical merge axis for vocab-parallel logprob. This PR does not implement a new logprob reducer or fused kernel, GRPO loss reduction, PP/DP validation, or rollout-versus-training integration from WS3.

Changes

Distributed topology runner

Add rl_engine/testing/distributed_logprob_comparison.py with:

  • A validated DistributedLogprobCase for TP/CP topology, dtype, vocabulary, tiles, shape, mask, and seed.
  • Deterministic planning for the six scoped TP/CP combinations.
  • Stable global-rank mapping:
tp_rank = global_rank % tp_world_size
cp_rank = global_rank // tp_world_size
world_size = tp_world_size * cp_world_size
  • Contiguous token-row partitioning across CP ranks.
  • Complete vocabulary-tile partitioning across TP ranks.
  • One TP process group per CP shard.
  • Direct execution as a kernel-local torchrun entry point.

Each invocation materializes exactly one topology. WORLD_SIZE must equal TP * CP; a mismatched launch fails before the process group is used.

Testing-module boundary

Keep the distributed entry point separate from rl_engine/testing/logprob_comparison.py intentionally. The PR2 module is a single-process, multi-backend comparison CLI, while the PR4 module is a torchrun entry point that materializes one explicit distributed backend and owns process groups, TP/CP topology, rank aggregation, and artifact writing. Both remain kernel-local under rl_engine/testing/; no project-wide script is added.

This separation avoids changing the already reviewed PR2 CLI and avoids combining two different execution models into a single module of more than one thousand lines. If maintainers prefer one consolidated logprob testing entry point, the two modules can be combined behind explicit single-GPU and distributed subcommands during review.

Independent FP32 oracle

Every rank reconstructs the same seeded logical FP32 logits, targets, and active mask. The candidate path receives only its BF16 token/vocabulary shard, while the oracle computes directly from the complete real vocabulary:

LSE_ref  = torch.logsumexp(full_fp32_logits[..., :real_vocab], dim=-1)
logp_ref = selected_fp32_logit - LSE_ref

The oracle does not use the PR3 sharded merge implementation, so the candidate is not compared against itself.

Drift and replication checks

For LSE and active-token dlogp, report:

active_count
max_abs
mean_abs
p95_abs
p99_abs
max_rel
atol
rtol
passed

The worst-drift record also includes the global token position, target id, target-owner TP rank, candidate value, and reference value. Tolerances come from the existing #108 tolerance contract.

After the PR3 TP merge, every rank in the same TP group should hold the same LSE and selected logprob. The runner gathers those outputs and checks them with torch.equal. Aggregate statistics then use only tp_rank=0 from each CP shard, avoiding duplicate counting of TP replicas.

Fail-closed backend materialization

The distributed runner requires an explicit backend and records both the requested and actual implementation. It fails when:

  • auto or an empty backend is requested.
  • Registry dispatch falls back to another backend.
  • An exact backend request materializes as a different backend.
  • Rank topology does not match the declared TP/CP case.
  • Padded vocabulary and vocabulary tiles cannot be partitioned as declared.
  • Rank reports disagree on backend or reduction materialization.

Structured distributed artifacts

Global rank zero writes a #116-style JSON artifact containing:

  • Stable case id and all case inputs.
  • Exact launch command.
  • Python, PyTorch, CUDA, and distributed-backend environment.
  • Per-rank TP/CP coordinates and token/vocabulary shard bounds.
  • Requested and actual backend, capability, fallback status, and contract fingerprint.
  • Reduction and communication provenance.
  • Per-rank and aggregate LSE/dlogp drift.
  • Bitwise TP-replication status and final pass/fail.

A failed numerical comparison still writes the artifact, then exits nonzero on global rank zero.

Operator documentation

Document the distributed topology contract, oracle, report fields, launch commands, and validation status in docs/operators/batch-invariant-logp.md.

Comparison contract

TP is the only numerical merge axis. CP partitions independent token rows and is used only when reconstructing the full logical report:

full logical logits
  -> CP token-row shard
  -> TP vocabulary shard
  -> BF16 PR3 candidate with fixed-order TP merge
  -> per-rank logp/LSE

full logical FP32 logits
  -> complete real-vocabulary torch.logsumexp
  -> independent reference logp/LSE

LSE drift includes every logical token row. Selected-logprob drift includes only active response/action tokens. Prompt and ignored rows do not contribute to dlogp statistics.

Tests

Add tests/test_distributed_logprob_comparison.py with coverage for:

  • The full TP=1/2/4 x CP=1/2 topology plan.
  • Global-rank to TP/CP-rank mapping.
  • Token and vocabulary shard coverage.
  • Rejection of auto backend and non-tileable vocabulary cases.
  • Reproducible launch-command generation.
  • Active-token mask drift-statistics semantics.
  • TP=1 CPU/Gloo execution and JSON artifact generation.
  • World-size mismatch failure before process-group initialization.
  • A real four-process CPU/Gloo TP=2, CP=2 subprocess run.
  • Per-rank report collection, TP replication checks, backend provenance, and aggregate active-token counts.

The CPU-safe PR2 and PR4 comparison suites are wired into .github/workflows/ci.yml, including the four-process Gloo case.

Validation

WSL focused PR1-PR4 suite

/mnt/d/Work/RL/new-folk/.venv-wsl/bin/python -m pytest -q \
  tests/test_logprob_contract.py \
  tests/test_vocab_parallel_logp.py \
  tests/test_logprob_comparison.py \
  tests/test_distributed_logprob_comparison.py

Result:

86 passed, 12 skipped in 49.18s

This includes the real four-process CPU/Gloo TP=2, CP=2 test. The skipped tests require CUDA, Triton, or a compiled CUDA extension.

Batch-invariant and vocab-parallel regression

/mnt/d/Work/RL/new-folk/.venv-wsl/bin/python -m pytest -q \
  tests/test_batch_invariant_logp.py \
  tests/test_vocab_parallel_logp.py

Result:

68 passed, 25 skipped in 18.07s

The skipped tests are GPU/backend-specific in the current WSL environment.

Static checks

Black: passed
isort: passed
Flake8: passed
Ruff: passed
MyPy: no issues in the three changed source modules
pre-commit on PR4 files: passed
Python compileall: passed
git diff --check: passed

Pending CUDA/NCCL validation

The distributed control flow and artifacts have been validated with CPU/Gloo. The scoped BF16 numerical gate still needs a real CUDA/NCCL run on H800/H100-class hardware with Qwen3 vocabulary size 151936:

TP CP Required ranks Status
1 1 1 Pending CUDA/NCCL
1 2 2 Pending CUDA/NCCL
2 1 2 Pending CUDA/NCCL
2 2 4 Pending CUDA/NCCL
4 1 4 Pending CUDA/NCCL
4 2 8 Pending CUDA/NCCL

Print the exact single-node launch matrix:

python rl_engine/testing/distributed_logprob_comparison.py \
  --plan \
  --device cuda \
  --dtype bf16 \
  --real-vocab 151936 \
  --padded-vocab 151936 \
  --num-vocab-tiles 64 \
  --output artifacts/ws2-logprob/report.json

Example four-GPU target case:

torchrun --standalone --nproc-per-node=4 \
  rl_engine/testing/distributed_logprob_comparison.py \
  --tp 2 \
  --cp 2 \
  --dtype bf16 \
  --backend pytorch-vocab-parallel-logp-ws2 \
  --real-vocab 151936 \
  --padded-vocab 151936 \
  --num-vocab-tiles 64 \
  --batch 2 \
  --seq 16 \
  --prompt-tokens 8 \
  --device cuda \
  --dist-backend nccl \
  --output artifacts/ws2-logprob/tp2-cp2.json

The full matrix requires eight ranks for TP=4, CP=2. Multi-node launch compatibility is supported by standard torchrun rendezvous arguments, but multi-node NCCL has not yet been validated and is not required if an eight-GPU single-node host is available.

Notes for review

  • This PR depends on the contract/dispatch work from PR1, the TP=1 harness from PR2, and the deterministic vocab-parallel reference from PR3.
  • The main addition is distributed integration, validation, and reporting; it does not replace or duplicate the PR3 numerical reducer.
  • The distributed runner is a separate testing module because its torchrun lifecycle and topology/artifact responsibilities differ from the PR2 single-process comparison CLI. This boundary can be consolidated if maintainers prefer one entry point.
  • CP is deliberately recorded as a non-merge axis for this operator.
  • Explicit backend requests fail rather than silently falling back because backend provenance is part of the acceptance contract.
  • CPU/Gloo validates topology, process groups, aggregation, artifacts, and failure behavior. It is not presented as a substitute for the pending BF16 CUDA/NCCL numerical matrix.

ryankert01 and others added 26 commits August 2, 2026 22:51
Implements PR 1 of issue RL-Align#241: a typed contract for vocab-parallel
selected-token logprob, mirroring the WS2 attention contract pattern.

- rl_engine/kernels/logprob_contract.py: LogprobContract, ShardingSpec
  (per-rank vocab shard bounds, padded-vs-real vocab, TP/CP rank
  metadata, owner_rank resolution), MaskSpec (active-token mask,
  ignore_index), ReductionSpec (fp32 (max, sumexp) merge in fixed
  global vocab-shard index order, all-gather transport, CP declared a
  non-merge axis), and LogprobBackendCapability.
- KernelRegistry.get_logprob_op(contract): contract-aware dispatch that
  only selects backends with a declared capability; incompatible or
  undeclared candidates are rejected with explicit reasons and never
  used as a silent fallback. Existing WS1 batch-invariant logp backends
  are declared truthfully as single-shard references, so strict WS2
  requests fail loudly until the deterministic vocab-parallel TP
  reference (PR 3) lands. Legacy get_op() behavior is unchanged.
- Design doc, runtime-dispatch and operator doc updates, and CPU-safe
  contract/dispatch tests covering the Qwen3-8B TP=2 BF16 target and
  the TP=1/2/4 sweep shapes. Tolerance values remain owned by RL-Align#108.
- docs: correct the TP-invariance claim — fixed merge order gives
  determinism per TP degree; cross-degree bitwise equality additionally
  requires a TP-degree-independent local tile decomposition (PR 3
  obligation), otherwise RL-Align#108 tolerances apply
- contract: store backend_id stripped so id-based dispatch matches;
  summarize the active mask in to_dict() provenance instead of copying
  every per-token boolean; sort __all__ per RUF022
- registry: add public register_logprob_backend() seam for PR 3 and
  tests; delegate _platform() to _platform_for_device(None); reuse
  _get_or_create_backend() in get_op so WS2 and legacy dispatch share
  one cache/blacklist code path
- tests: use the registration seam instead of poking private state,
  pin _even_bounds' last bound for non-divisible vocabularies, assert
  candidate-list decoupling in both directions, cover registration
  replace semantics and backend_id normalization
- docs: state that cross-TP bitwise equality needs a global tile-level
  merge structure independent of TP partitioning (per-shard tiles alone
  leave different grouping at shard boundaries), and that padded columns
  are masked to -inf before the local (max, sumexp) partials
- registry: scope logprob capabilities per platform so the same backend
  enum can declare different support on cuda/rocm/cpu; validate the
  platform argument of register_logprob_backend against known platforms
- contract: derive IMPLEMENTATION_KINDS from RESERVED_DISPATCH_POLICIES
  and use it for the kind check; wrap non-iterable roles/dtypes in
  LogprobContractError for consistent error handling
- tests: cover per-platform capability scoping, unknown-platform
  rejection, and non-iterable roles/dtypes
…typed contract

Address external review: the cross-TP bitwise guarantee lived only in
prose, so a fixed-topology-deterministic backend could pass dispatch as
fully conformant.

- DeterminismScope (fixed_topology | cross_tp_bitwise): requested via
  ReductionSpec (default cross_tp_bitwise, the RL-Align#241 PR 3 target),
  declared per backend via determinism_scopes, enforced by dispatch;
  replaces the deterministic_tp_merge bool
- MaskMode (explicit_active_mask | ignore_index) replaces
  supports_inactive_tokens: the contract permits inactive targets that
  do not hold ignore_index, so ignore-index-only backends are rejected
  for contracts with inactive tokens
- LogprobOutputSpec pins the output surface: fp32 selected logprob and
  fp32 vocab LSE, replicated across the TP group
- implementation_kind is now a tier (reference | production);
  determinism is no longer conflated with it, and requesting
  "deterministic" as a policy raises a loud error pointing at
  determinism_scope
- fallback provenance: policy evaluation now precedes capability
  checks, so a candidate excluded by the caller's own policy never
  counts as a fallback even when it also lacks capabilities
- docs: define the (-inf, 0) identity partial for padding-only or
  all--inf shards; document that requested_backend="auto" is not
  distributed-safe and specify the preflight fingerprint agreement
- LogprobContract.cross_rank_fingerprint(): rank-independent identity
  for that preflight; provenance now records active_mask_sha256 so
  masks with equal active counts remain distinguishable
Fold the normative reduction semantics (padded-column masking, fp32
(max, sumexp) merge formulas, the (-inf, 0) identity partial, and the
cross-TP tile-structure requirement) into the ReductionSpec and
DeterminismScope docstrings, and repoint the runtime-dispatch and
batch-invariant-logp doc references at the module. The contract summary
moves to the PR description.
Shrink class docstrings toward the attention-contract one-liner style and
cut design-rationale comments; the normative reduction semantics stay in
the ReductionSpec and DeterminismScope docstrings.
The guard added per review rejects requested_backend="auto" whenever
tp_world_size > 1, so TP-sharded dispatch tests now name an explicit
policy and auto-policy tests use TP=1 contracts.  Add coverage for the
guard itself and document the restriction in get_logprob_op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PyjQEqDJwy9Cos4Sb9QBK
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added WS2 tensor-parallel logprob contracts, deterministic vocabulary-parallel execution, capability-aware dispatch, direct LSE diagnostics, distributed comparison tooling, documentation, tests, and CI coverage.

Changes

WS2 logprob

Layer / File(s) Summary
Contracts and dispatch
rl_engine/kernels/logprob_contract.py, rl_engine/kernels/registry.py, docs/design/runtime-dispatch.md, tests/test_logprob_contract.py
Defines validated contract metadata, backend capabilities, compatibility checks, provenance fingerprints, and strict WS2 dispatch behavior.
Logprob operators
rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py, rl_engine/kernels/ops/pytorch/loss/batch_invariant_logp.py, rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py, rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py, tests/test_vocab_parallel_logp.py, docs/operators/batch-invariant-logp.md
Adds deterministic TP vocabulary reductions, selected-target ownership, padding and inactive-row handling, custom gradients, and direct LSE outputs.
Comparison and validation tooling
rl_engine/testing/*logprob*, rl_engine/testing/__init__.py, tests/test_logprob_comparison.py, tests/test_distributed_logprob_comparison.py, .github/workflows/ci.yml
Adds single-GPU and distributed comparison CLIs, drift metrics, JSON reports, topology validation, provenance checks, integration tests, documentation, exports, and CI execution steps.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant KernelRegistry
  participant VocabParallelLogprobOp
  participant TPGroup
  participant ComparisonReport
  Caller->>KernelRegistry: request contract-aware logprob backend
  KernelRegistry->>VocabParallelLogprobOp: materialize selected backend
  VocabParallelLogprobOp->>TPGroup: reduce vocabulary tile statistics
  TPGroup-->>VocabParallelLogprobOp: return replicated LSE and logprob outputs
  VocabParallelLogprobOp-->>ComparisonReport: provide outputs and provenance
Loading

Possibly related issues

Possibly related PRs

  • RL-Align/RL-Kernel#265 — Directly relates to the deterministic vocab-parallel implementation and its dispatch and tests.
  • RL-Align/RL-Kernel#262 — Provides related TP=1 comparison and forward_with_lse functionality.
  • RL-Align/RL-Kernel#259 — Provides related contract-aware logprob dispatch extended here with vocab-parallel support.

Suggested labels: needs-gpu-ci

Suggested reviewers: bitborne, ethanzero2hero, inaniloquentee

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a distributed TP/CP logprob drift report for WS2 PR4.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
tests/test_logprob_comparison.py (1)

204-224: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to this subprocess call.

subprocess.run has no timeout here. If the child process blocks, the test blocks until the CI job limit. The distributed CLI test at tests/test_distributed_logprob_comparison.py line 206 already sets timeout=120. Match that behavior.

The Ruff S603 and ast-grep injection hints on this call are false positives. The argument list is literal, and no shell is used.

♻️ Proposed change
         check=True,
         capture_output=True,
         text=True,
+        timeout=120,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_logprob_comparison.py` around lines 204 - 224, Add timeout=120 to
the subprocess.run call in the test using the literal argument list, matching
the distributed CLI test’s timeout behavior; leave the existing check, output
capture, and shell-free invocation unchanged.

Source: Linters/SAST tools

tests/test_distributed_logprob_comparison.py (1)

172-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface child stderr when this four-process run fails.

check=True raises CalledProcessError, and that message omits the captured stderr. torchrun writes per-rank tracebacks to stderr. Without them, a CI failure of this test reports only a return code.

♻️ Proposed change
-        check=True,
+        check=False,
         capture_output=True,
         text=True,
         timeout=120,
         env=environment,
     )
+    assert result.returncode == 0, result.stderr
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_distributed_logprob_comparison.py` around lines 172 - 208, Update
the subprocess invocation in the distributed run test to surface captured child
stderr when torch.distributed.run fails. Preserve check=True and the existing
capture behavior, but catch the resulting CalledProcessError and include its
stderr in the test failure output.
rl_engine/testing/distributed_logprob_comparison.py (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import Sequence from collections.abc.

The project supports Python 3.10+, and the sibling module already uses this import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/testing/distributed_logprob_comparison.py` at line 17, Update the
typing imports in distributed_logprob_comparison.py to import Sequence from
collections.abc instead of typing, while retaining Any from typing and leaving
the module’s usage unchanged.
rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py (1)

263-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Each new forward_with_lse entry point re-implements the validation already present in apply. Both backends added a diagnostic path with its own copy of the device, dimensionality, shape, and target-range checks, so the two copies in each file can diverge.

  • rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py#L263-L295: call the new _validate_inputs helper from apply (lines 210-243) instead of keeping the inline copy.
  • rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py#L178-L202: extract the checks into one static validator and call it from both apply (lines 138-161) and forward_with_lse.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py` around lines 263 -
295, The Triton validation is duplicated instead of reused, and the CUDA backend
also needs a shared validator. In
rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py:263-295, update apply
to call the existing _validate_inputs helper and remove its inline checks. In
rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py:178-202, extract the
validation into one static validator and call it from both apply and
forward_with_lse.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/operators/batch-invariant-logp.md`:
- Around line 57-62: Update the VocabParallelLogprobOp validation statement to
make bit-identical results conditional rather than unconditional: specify that
it applies when num_vocab_tiles is fixed and all shard bounds are tile-aligned,
and acknowledge that CUDA/NCCL BF16 validation remains pending.

In `@rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`:
- Around line 378-401: Make local validation in the vocab-parallel logprob entry
point collective when contract.sharding.tp_world_size > 1: capture failures from
_tile_size, _validate_invocation, and _validate_active_targets, exchange
per-rank success or error status with the existing
_preflight_cross_rank_agreement all_gather_object mechanism, and raise the
corresponding LogprobContractError on every rank before any later collective or
_VocabParallelLogprobFunction.apply call. Preserve single-rank behavior and
avoid entering subsequent collectives when any rank reports validation failure.

In `@rl_engine/testing/distributed_logprob_comparison.py`:
- Around line 621-627: Update run_distributed_logprob_case to accept a
keyword-only collective_timeout_s parameter defaulting to 300, expose it through
the --collective-timeout CLI option, and pass
datetime.timedelta(seconds=collective_timeout_s) as timeout to
dist.init_process_group in the initialization block. Ensure the CLI value is
forwarded to run_distributed_logprob_case for both gloo and nccl runs.
- Around line 386-405: Update the relative-error calculation in the drift
comparison flow around selected_diff and selected_ref to clamp the denominator
to a representable epsilon, preventing max_rel overflow for near-zero
references. Also update both JSON serialization write sites using json.dumps to
pass allow_nan=False so non-finite report values fail loudly instead of emitting
invalid JSON.

In `@tests/test_vocab_parallel_logp.py`:
- Around line 179-192: Remove the initial overwritten contract assignment in
test_matches_ws1_batch_invariant_logp_within_contract_tolerance, keeping only
the real_vocab=PADDED_VOCAB and padded_vocab=PADDED_VOCAB contract used by the
test.

---

Nitpick comments:
In `@rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py`:
- Around line 263-295: The Triton validation is duplicated instead of reused,
and the CUDA backend also needs a shared validator. In
rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py:263-295, update apply
to call the existing _validate_inputs helper and remove its inline checks. In
rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py:178-202, extract the
validation into one static validator and call it from both apply and
forward_with_lse.

In `@rl_engine/testing/distributed_logprob_comparison.py`:
- Line 17: Update the typing imports in distributed_logprob_comparison.py to
import Sequence from collections.abc instead of typing, while retaining Any from
typing and leaving the module’s usage unchanged.

In `@tests/test_distributed_logprob_comparison.py`:
- Around line 172-208: Update the subprocess invocation in the distributed run
test to surface captured child stderr when torch.distributed.run fails. Preserve
check=True and the existing capture behavior, but catch the resulting
CalledProcessError and include its stderr in the test failure output.

In `@tests/test_logprob_comparison.py`:
- Around line 204-224: Add timeout=120 to the subprocess.run call in the test
using the literal argument list, matching the distributed CLI test’s timeout
behavior; leave the existing check, output capture, and shell-free invocation
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e5c280f-d98e-48fb-a4a9-61cfe9df7c9f

📥 Commits

Reviewing files that changed from the base of the PR and between 505512d and 1d9bac1.

📒 Files selected for processing (17)
  • .github/workflows/ci.yml
  • docs/design/runtime-dispatch.md
  • docs/operators/batch-invariant-logp.md
  • rl_engine/kernels/logprob_contract.py
  • rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py
  • rl_engine/kernels/ops/pytorch/loss/batch_invariant_logp.py
  • rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py
  • rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py
  • rl_engine/kernels/registry.py
  • rl_engine/testing/__init__.py
  • rl_engine/testing/distributed_logprob_comparison.py
  • rl_engine/testing/logprob_comparison.py
  • rl_engine/testing/logprob_drift.py
  • tests/test_distributed_logprob_comparison.py
  • tests/test_logprob_comparison.py
  • tests/test_logprob_contract.py
  • tests/test_vocab_parallel_logp.py

Comment thread docs/operators/batch-invariant-logp.md
Comment on lines +378 to +401
if not isinstance(contract, LogprobContract):
raise LogprobContractError("contract must be a LogprobContract")
tile = _tile_size(contract, num_vocab_tiles)
_validate_invocation(local_logits, target_ids, contract, tp_group)

target_1d = target_ids.reshape(-1).to(device=local_logits.device, dtype=torch.long)
active_mask = torch.tensor(
contract.mask.active_mask, dtype=torch.bool, device=local_logits.device
)
if validate:
_validate_active_targets(target_1d, active_mask, contract.sharding.real_vocab_size)
if contract.sharding.tp_world_size > 1:
_preflight_cross_rank_agreement(contract, tp_group, num_vocab_tiles)

selected_logp, lse = _VocabParallelLogprobFunction.apply(
local_logits, target_1d, active_mask, contract, tp_group, tile
)

if validate and bool((~torch.isfinite(lse) & active_mask).any().item()):
raise LogprobContractError(
"non-finite logsumexp on an active row: logits over the real "
"vocabulary must be finite for every active token"
)
return selected_logp, lse

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Rank-local validation failures can strand the other TP ranks in a collective.

_tile_size, _validate_invocation, and _validate_active_targets all raise from local state only. If one rank fails one of these checks and the other ranks pass, the failing rank returns from apply while the remaining ranks proceed into _preflight_cross_rank_agreement (all_gather_object) or into the forward all_gather. Those ranks then block until the process-group timeout.

The current tests only cover symmetric failures: _case_tile_misaligned_bounds perturbs the bounds on every rank, and the preflight scenario still lets every rank enter all_gather_object. An asymmetric failure, for example a wrong local shard width on a single rank, is not covered and hangs.

Consider converting local validation into a collective decision when tp_world_size > 1: gather a per-rank status (or the validation error text) with the same all_gather_object payload already used by the preflight, then raise on every rank.

🛡️ Sketch: fold local validation results into the existing preflight exchange
 def _preflight_cross_rank_agreement(
-    contract: LogprobContract, tp_group: Any, num_vocab_tiles: int
+    contract: LogprobContract, tp_group: Any, num_vocab_tiles: int, local_error: str | None = None
 ) -> None:
     dist = _require_distributed_initialized()
     payload = (contract.cross_rank_fingerprint(), BACKEND_ID, int(num_vocab_tiles))
     world = dist.get_world_size(group=tp_group)
-    gathered: list[Any] = [None] * world
-    dist.all_gather_object(gathered, payload, group=tp_group)
+    gathered: list[Any] = [None] * world
+    dist.all_gather_object(gathered, (payload, local_error), group=tp_group)
+    failures = [(rank, err) for rank, (_, err) in enumerate(gathered) if err]
+    if failures:
+        raise LogprobContractError(f"rank-local validation failed on ranks: {failures}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py` around lines 378 -
401, Make local validation in the vocab-parallel logprob entry point collective
when contract.sharding.tp_world_size > 1: capture failures from _tile_size,
_validate_invocation, and _validate_active_targets, exchange per-rank success or
error status with the existing _preflight_cross_rank_agreement all_gather_object
mechanism, and raise the corresponding LogprobContractError on every rank before
any later collective or _VocabParallelLogprobFunction.apply call. Preserve
single-rank behavior and avoid entering subsequent collectives when any rank
reports validation failure.

Comment thread rl_engine/testing/distributed_logprob_comparison.py
Comment thread rl_engine/testing/distributed_logprob_comparison.py Outdated
Comment on lines +179 to +192
def test_matches_ws1_batch_invariant_logp_within_contract_tolerance(self):
tolerance = load_contract()["accuracy"]["default"]["logprob"]["float32"]
contract = _contract(padded_vocab=REAL_VOCAB + 5)
# Use a real==padded contract so the WS1 op sees identical logits.
contract = _contract(real_vocab=PADDED_VOCAB, padded_vocab=PADDED_VOCAB)
logits, targets = _inputs()
logp, _ = VocabParallelLogprobOp()(
logits, targets, contract=contract, num_vocab_tiles=NUM_TILES
)
ws1 = NativeBatchInvariantLogpOp().apply(logits, targets)
active = torch.tensor(ACTIVE)
assert torch.allclose(
logp[active], ws1[active], atol=tolerance["atol"], rtol=tolerance["rtol"]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the overwritten contract assignment.

Line 181 builds a contract with padded_vocab=REAL_VOCAB + 5, and line 183 immediately replaces it. The first assignment has no effect and suggests a padded-vocab case is covered here when it is not.

🧹 Proposed cleanup
         tolerance = load_contract()["accuracy"]["default"]["logprob"]["float32"]
-        contract = _contract(padded_vocab=REAL_VOCAB + 5)
         # Use a real==padded contract so the WS1 op sees identical logits.
         contract = _contract(real_vocab=PADDED_VOCAB, padded_vocab=PADDED_VOCAB)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_matches_ws1_batch_invariant_logp_within_contract_tolerance(self):
tolerance = load_contract()["accuracy"]["default"]["logprob"]["float32"]
contract = _contract(padded_vocab=REAL_VOCAB + 5)
# Use a real==padded contract so the WS1 op sees identical logits.
contract = _contract(real_vocab=PADDED_VOCAB, padded_vocab=PADDED_VOCAB)
logits, targets = _inputs()
logp, _ = VocabParallelLogprobOp()(
logits, targets, contract=contract, num_vocab_tiles=NUM_TILES
)
ws1 = NativeBatchInvariantLogpOp().apply(logits, targets)
active = torch.tensor(ACTIVE)
assert torch.allclose(
logp[active], ws1[active], atol=tolerance["atol"], rtol=tolerance["rtol"]
)
def test_matches_ws1_batch_invariant_logp_within_contract_tolerance(self):
tolerance = load_contract()["accuracy"]["default"]["logprob"]["float32"]
# Use a real==padded contract so the WS1 op sees identical logits.
contract = _contract(real_vocab=PADDED_VOCAB, padded_vocab=PADDED_VOCAB)
logits, targets = _inputs()
logp, _ = VocabParallelLogprobOp()(
logits, targets, contract=contract, num_vocab_tiles=NUM_TILES
)
ws1 = NativeBatchInvariantLogpOp().apply(logits, targets)
active = torch.tensor(ACTIVE)
assert torch.allclose(
logp[active], ws1[active], atol=tolerance["atol"], rtol=tolerance["rtol"]
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_vocab_parallel_logp.py` around lines 179 - 192, Remove the initial
overwritten contract assignment in
test_matches_ws1_batch_invariant_logp_within_contract_tolerance, keeping only
the real_vocab=PADDED_VOCAB and padded_vocab=PADDED_VOCAB contract used by the
test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rl_engine/testing/distributed_logprob_comparison.py`:
- Line 630: Move the existing try/finally scope in the process-group setup flow
so it begins before dist.init_process_group and includes initialization, device
setup, rank_topology, _create_tp_group, and execution. Preserve the cleanup in
the matching finally so failures during any setup step destroy the process group
and reset state.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6116c6d3-092f-483b-a355-074f4179aa9b

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9bac1 and f6b5a07.

📒 Files selected for processing (3)
  • docs/operators/batch-invariant-logp.md
  • rl_engine/testing/distributed_logprob_comparison.py
  • tests/test_distributed_logprob_comparison.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/operators/batch-invariant-logp.md
  • tests/test_distributed_logprob_comparison.py

Comment thread rl_engine/testing/distributed_logprob_comparison.py Outdated
@hihaluemen
hihaluemen changed the base branch from main to test August 12, 2026 12:05
@KJLdefeated KJLdefeated changed the title [WS2] feat: add distributed TP/CP logprob drift report [WS2][PR4][Logp] feat: add distributed TP/CP logprob drift report Aug 13, 2026
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