Skip to content

[WS2] feat: add TP=1 logprob comparison harness - #262

Open
hihaluemen wants to merge 10 commits into
RL-Align:mainfrom
hihaluemen:feat/ws2-logprob-single-gpu-harness-pr2
Open

[WS2] feat: add TP=1 logprob comparison harness#262
hihaluemen wants to merge 10 commits into
RL-Align:mainfrom
hihaluemen:feat/ws2-logprob-single-gpu-harness-pr2

Conversation

@hihaluemen

@hihaluemen hihaluemen commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Add the TP=1 logprob comparison harness requested by PR2 of #241.

The harness registers the existing WS1 batch-invariant PyTorch logprob path as the reference and compares the supported single-GPU backends against it before tensor-parallel communication is introduced. It reports direct vocabulary-LSE drift and active-token-only selected-logprob drift, while recording enough backend provenance to detect accidental fallback.

The existing production operator contract remains unchanged. The new forward_with_lse methods are diagnostic entry points used by the comparison harness and tests.

Implements PR2 of #241.

Scope

This PR covers the single-GPU registration and regression guard described in PR2:

  • Use the merged WS1 batch-invariant PyTorch implementation as the reference.
  • Require the TP=1 PyTorch path to remain bitwise equal to that reference.
  • Compare the PyTorch, Triton, and CUDA SM90 batch-invariant logprob backends.
  • Report vocabulary-LSE drift over all logical token rows.
  • Report selected-token dlogp drift over active response/action tokens only.
  • Record tp_world=1 and communication=none in the report.
  • Fail closed when an explicitly requested backend is unavailable or falls back to another implementation.

This PR does not implement vocab sharding, collective communication, fixed-order cross-rank LSE merging, CP reconstruction, or distributed artifact generation. Those remain part of the later PRs in #241.

Changes

Single-GPU comparison harness

Add rl_engine/testing/logprob_comparison.py with:

  • Structured comparison inputs, candidates, reports, and backend provenance.
  • Exact backend selection for pytorch, triton, and cuda-sm90.
  • Direct comparison against the existing batch-invariant PyTorch reference.
  • Bitwise equality reporting for selected logprobs.
  • Max, mean, p95, and p99 absolute-drift statistics.
  • Active-token masking for selected-logprob drift.
  • Validation for target shape, dtype, range, and ignore_index usage.
  • A generic operator-comparison registration for batch_invariant_logp.

Diagnostic LSE entry points

Add a diagnostic-only method to each supported backend:

op.forward_with_lse(logits, target_ids, ignore_index=-100) -> (logp, lse)

The normal production call remains:

op(logits, target_ids, ignore_index=-100) -> logp

The diagnostic path exposes the LSE computed by the backend itself. The harness does not reconstruct LSE from selected logprobs, which keeps the LSE comparison independent and useful for later TP work.

For an explicit cuda-sm90 request, the diagnostic path requires the compiled SM90 extension and compatible Hopper inputs. It does not use the production operator's fallback behavior.

Kernel-local command-line entry

Make rl_engine/testing/logprob_comparison.py directly executable for reproducible local
and GPU comparisons. This keeps the kernel-specific diagnostic next to its harness instead
of adding a project-wide script.

Example:

python rl_engine/testing/logprob_comparison.py \
  --candidate triton \
  --candidate cuda-sm90 \
  --device cuda \
  --dtype bf16 \
  --batch 2 \
  --seq 16 \
  --vocab 151936

The command writes a structured JSON report to stdout. RL-Kernel diagnostic logs are routed to stderr so redirected stdout remains valid machine-readable JSON.

Operator documentation

Document the TP=1 harness contract, exact backend selection, CLI usage, and SM90 validation
in docs/operators/batch-invariant-logp.md. The content is consolidated into the existing
operator documentation rather than maintained as separate WS2 design documents.

Comparison contract

For each logical token row, the compared values are:

LSE  = logsumexp(logits[..., vocab])
logp = selected_logit - LSE

LSE drift is measured over every logical token row. Selected-logprob drift is measured only where the active-token mask is true. Each drift report contains:

active_count
max_abs
mean_abs
p95_abs
p99_abs

The report also records:

  • Requested and actual backend.
  • Concrete implementation class.
  • Direct-LSE provenance.
  • Input shape and dtype.
  • Active-token count.
  • TP world size.
  • Communication mode.
  • Bitwise selected-logprob status.

Tests

Add focused coverage for:

  • TP=1 PyTorch bitwise regression.
  • Direct LSE identity.
  • Active-token-only percentile calculation.
  • The zero-active-token case.
  • Invalid active ignore_index usage.
  • Structured report serialization.
  • Operator-harness registration.
  • Exact Triton and CUDA SM90 diagnostic paths.
  • Fail-closed backend provenance.
  • Machine-readable CLI stdout when RL-Kernel emits diagnostic logs.

Validation

Windows CPU

python -m pytest tests/test_logprob_comparison.py tests/test_operator_inputs.py tests/test_op_checks.py -q

Result:

39 passed, 2 skipped

The skipped cases require CUDA/Triton backends.

WSL

Current focused and complete operator validation:

PR2 focused tests: 42 passed, 1 skipped
Complete batch-invariant logprob suite: 53 passed, 14 skipped

The skipped cases require a compiled CUDA SM90 extension on Hopper hardware. The focused
suite includes a subprocess test of the direct module CLI and machine-readable JSON output.

NVIDIA H800 / SM90

Validated on:

GPU: NVIDIA H800 PCIe
Compute capability: 9.0
Python: 3.11.15
PyTorch: 2.11.0+cu128
CUDA toolkit / nvcc: 12.8
Triton: 3.6.0

The editable CUDA extension built successfully with the SM90 kernel enabled:

batch_invariant_logp_sm90=True

Test results:

PR2 focused tests: 41 passed in 3.25s
Complete batch-invariant logprob suite: 67 passed in 4.60s

Observed BF16 SM90 drift against the PyTorch reference:

Shape LSE max abs dlogp max abs
[2, 8, 1024] 4.76837158203125e-07 4.76837158203125e-07
[2, 16, 151936] 9.5367431640625e-07 9.5367431640625e-07

Both comparisons used tp_world=1, communication=none, and the requested cuda-sm90 implementation without fallback.

Additional checks:

MyPy: no issues found in 90 source files
pre-commit --all-files: passed
Python compileall: passed
git diff --check: passed

Notes for review

  • The main addition is the comparison and reporting harness; this PR does not change distributed logprob mathematics.
  • Production calls still return only selected logprobs.
  • forward_with_lse exists to expose backend-native diagnostics without changing production callers.
  • Explicit backend requests intentionally fail instead of silently falling back, because backend provenance is part of the regression contract.
  • The PyTorch bitwise check is the TP=1 regression guard requested by [WS2] TP-aware deterministic logprob for cross-config alignment (Qwen3-8B TP=2 CP=2 BF16) #241; numerical drift is expected for independently implemented Triton and CUDA reductions.

Summary by CodeRabbit

  • New Features

    • Added access to both selected log-probabilities and row-wise log-sum-exp values across supported PyTorch, Triton, and SM90 CUDA implementations.
    • Added a single-GPU comparison tool for measuring numerical drift between log-probability backends.
    • Added structured comparison reports, provenance details, and command-line output.
  • Documentation

    • Added usage, validation, build, and testing guidance for the comparison workflow.
  • Tests

    • Expanded coverage for backend validation, diagnostics, drift reporting, and command-line behavior.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added forward_with_lse diagnostics to native, Triton, and SM90 logprob operators. Added a single-GPU comparison API, CLI, provenance checks, drift metrics, tests, and SM90 validation documentation.

Changes

Single-GPU logprob comparison

Layer / File(s) Summary
Operator LSE diagnostics
rl_engine/kernels/ops/{pytorch,triton,cuda}/loss/batch_invariant_logp.py
Native, Triton, and SM90 operators now return selected log-probabilities with row-wise LSE values through forward_with_lse. Triton validation and kernel launch logic are shared.
Comparison API and provenance
rl_engine/testing/logprob_comparison.py, rl_engine/testing/__init__.py
Added backend selection, native reference execution, output validation, provenance tracking, drift statistics, input validation, report serialization, and public exports.
CLI comparison harness
rl_engine/testing/logprob_comparison.py
Added seeded input generation, device and dtype selection, backend execution, stderr logging, and JSON report output.
Validation coverage and maintainer workflow
tests/test_logprob_comparison.py, docs/operators/batch-invariant-logp.md
Added PyTorch, CLI, provenance, operator-suite, Triton, CUDA, and SM90 diagnostics, with corresponding harness and validation documentation.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ComparisonHarness
  participant ReferenceBackend
  participant CandidateBackend
  participant JSONReport
  CLI->>ComparisonHarness: create seeded inputs
  ComparisonHarness->>ReferenceBackend: compute logprob and LSE
  ComparisonHarness->>CandidateBackend: execute selected backend
  CandidateBackend-->>ComparisonHarness: return logprob and LSE
  ComparisonHarness->>JSONReport: calculate drift and provenance
  JSONReport-->>CLI: emit serialized JSON
Loading

Possibly related issues

Possibly related PRs

Suggested labels: needs-gpu-ci

Suggested reviewers: inaniloquentee, flink-ddd, ethanzero2hero

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding a TP=1 logprob comparison harness.
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.
✨ 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: 2

🧹 Nitpick comments (1)
rl_engine/testing/logprob_comparison.py (1)

132-146: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Disable autograd during diagnostic execution.

If inputs.logits.requires_grad is true, the reference creates two autograd graphs, and a PyTorch candidate can create another graph. The later detach() calls occur after full-vocabulary intermediates are retained. Run diagnostic calls under torch.no_grad().

Proposed change
-    reference_logp, reference_lse = _run_ws1_reference(
-        inputs.logits, effective_targets, inputs.ignore_index
-    )
+    with torch.no_grad():
+        reference_logp, reference_lse = _run_ws1_reference(
+            inputs.logits, effective_targets, inputs.ignore_index
+        )
@@
-        logp, lse = _run_candidate(
-            candidate,
-            inputs.logits,
-            effective_targets,
-            inputs.ignore_index,
-        )
+        with torch.no_grad():
+            logp, lse = _run_candidate(
+                candidate,
+                inputs.logits,
+                effective_targets,
+                inputs.ignore_index,
+            )
🤖 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/logprob_comparison.py` around lines 132 - 146, The
diagnostic execution creates autograd graphs during _run_ws1_reference and
_run_candidate calls which retain full-vocabulary intermediates in memory, even
though detach() is applied later. Wrap the _validate_inputs call, the
_run_ws1_reference invocation, and the candidate iteration loop (containing the
_run_candidate calls) in a torch.no_grad() context manager to disable autograd
tracking entirely during these diagnostic operations.
🤖 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/logprob_comparison.py`:
- Around line 178-185: Run Black and isort formatters on the specified Python
files to match repository formatting standards. Apply isort to the imports and
Black to the code formatting at the following locations:
rl_engine/testing/logprob_comparison.py lines 178-185 (apply both isort to the
NativeBatchInvariantLogpOp import and Black to the op() and forward_with_lse()
call formatting), scripts/compare_logprob.py lines 19-22 (apply isort to the
package import), tests/test_logprob_comparison.py lines 15-17 (apply both isort
and Black to the import), and tests/test_logprob_comparison.py lines 197-199
(apply Black to the function call formatting).
- Around line 216-224: Update _candidate_provenance so candidate.provenance is
merged before the canonical requested_backend, actual_backend, tp_world,
communication, and lse_source fields. Ensure these canonical fields remain
authoritative and cannot be overwritten in the serialized report.

---

Nitpick comments:
In `@rl_engine/testing/logprob_comparison.py`:
- Around line 132-146: The diagnostic execution creates autograd graphs during
_run_ws1_reference and _run_candidate calls which retain full-vocabulary
intermediates in memory, even though detach() is applied later. Wrap the
_validate_inputs call, the _run_ws1_reference invocation, and the candidate
iteration loop (containing the _run_candidate calls) in a torch.no_grad()
context manager to disable autograd tracking entirely during these diagnostic
operations.
🪄 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: fae2870a-cf06-4b8c-9dbb-30a35b083445

📥 Commits

Reviewing files that changed from the base of the PR and between 0b12d34 and 115d86c.

📒 Files selected for processing (9)
  • docs/design/ws2-logprob-single-gpu-harness.md
  • docs/design/ws2-logprob-sm90-validation.md
  • rl_engine/kernels/ops/cuda/loss/batch_invariant_logp.py
  • rl_engine/kernels/ops/pytorch/loss/batch_invariant_logp.py
  • rl_engine/kernels/ops/triton/loss/batch_invariant_logp.py
  • rl_engine/testing/__init__.py
  • rl_engine/testing/logprob_comparison.py
  • scripts/compare_logprob.py
  • tests/test_logprob_comparison.py

Comment thread rl_engine/testing/logprob_comparison.py Outdated
Comment thread rl_engine/testing/logprob_comparison.py

@KJLdefeated KJLdefeated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks good to me. After addressed the request, I am happy to approve.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To let future maintenance easier, I think it is not necessary to create two docs for this. I suggest that you can compress the context of logprob harness and put into existing docs/operators/batch-invariant-logp.md.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as this one, compress the context and put to docs/operators/batch-invariant-logp.md.

Comment thread scripts/compare_logprob.py Outdated

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rl_engine/testing/logprob_comparison.py (1)

116-124: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve backend execution failures as failures.

LogprobBackendUnavailable is used by callers to skip unavailable backends. The wrapper converts RuntimeError, NotImplementedError, and OSError from forward_with_lse, so kernel bugs, unsupported inputs, CUDA/OOM errors, and similar execution failures can be treated as skip conditions. Introduce a dedicated unsupported-capability/missing-feature exception and let execution failures propagate.

🤖 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/logprob_comparison.py` around lines 116 - 124, Update run
so LogprobBackendUnavailable is raised only for a dedicated
unsupported-capability or missing-feature exception from the exact backend.
Remove the conversion of RuntimeError, NotImplementedError, and OSError; allow
those execution failures from diagnostic to propagate unchanged.
🧹 Nitpick comments (1)
rl_engine/testing/logprob_comparison.py (1)

304-309: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Redirect only the intended logging handlers.

logging.FileHandler is a subclass of logging.StreamHandler, so this condition can redirect file logging to sys.stderr. The loop also does not affect handlers reached through logger propagation. Inspect rl_engine.utils.logger.logger and target the intended stream handler explicitly.

🤖 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/logprob_comparison.py` around lines 304 - 309, Update
_route_rl_kernel_logs_to_stderr to target only the intended console/stream
handler, excluding logging.FileHandler, and preserve file-handler streams.
Inspect the configured handlers on logger, including propagated handlers if the
logger configuration relies on propagation, and redirect only the explicitly
identified stream handler to sys.stderr.
🤖 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 `@tests/test_logprob_comparison.py`:
- Around line 201-205: Update test_cli_runs_directly_from_testing_module to
derive the logprob_comparison.py subprocess path from Path(__file__).resolve(),
ensuring the script is found regardless of pytest’s working directory while
preserving the existing subprocess validation.

---

Outside diff comments:
In `@rl_engine/testing/logprob_comparison.py`:
- Around line 116-124: Update run so LogprobBackendUnavailable is raised only
for a dedicated unsupported-capability or missing-feature exception from the
exact backend. Remove the conversion of RuntimeError, NotImplementedError, and
OSError; allow those execution failures from diagnostic to propagate unchanged.

---

Nitpick comments:
In `@rl_engine/testing/logprob_comparison.py`:
- Around line 304-309: Update _route_rl_kernel_logs_to_stderr to target only the
intended console/stream handler, excluding logging.FileHandler, and preserve
file-handler streams. Inspect the configured handlers on logger, including
propagated handlers if the logger configuration relies on propagation, and
redirect only the explicitly identified stream handler to sys.stderr.
🪄 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: 7ee39833-edf9-4862-8e0e-ba514ba135bd

📥 Commits

Reviewing files that changed from the base of the PR and between 99e59f8 and 4eebb3b.

📒 Files selected for processing (3)
  • docs/operators/batch-invariant-logp.md
  • rl_engine/testing/logprob_comparison.py
  • tests/test_logprob_comparison.py

Comment thread tests/test_logprob_comparison.py Outdated
@hihaluemen

hihaluemen commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Overall looks good to me. After addressed the request, I am happy to approve.

@KJLdefeated Addressed, thanks for the suggestions. I consolidated both harness documents into docs/operators/batch-invariant-logp.md and moved the kernel-specific CLI into rl_engine/testing/logprob_comparison.py.

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.

2 participants