Skip to content
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ jobs:
run: |
python -m pytest tests/test_kv_cache_attention.py -v -k "not large and not gpu"

- name: Run WS2 Logprob Contract Tests (CPU-safe)
run: python -m pytest tests/test_logprob_contract.py -v

- name: Run WS2 Vocab-Parallel Logprob Tests (CPU-safe)
run: python -m pytest tests/test_vocab_parallel_logp.py -v
Comment on lines +82 to +83

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

Run multi-GPU logprob tests in CI.

This job installs the CPU-only PyTorch wheel and uses ubuntu-latest. The TP=2 and TP=4 NCCL tests skip on this runner. Add a GPU-backed CI job that runs the distributed test cases. Otherwise, CI does not validate the cross-TP bitwise-equality requirement.

🤖 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 @.github/workflows/ci.yml around lines 82 - 83, Add a GPU-backed CI job
alongside the existing CPU-safe “Run WS2 Vocab-Parallel Logprob Tests” step,
using a runner and PyTorch installation that support CUDA/NCCL. Configure it to
execute the distributed TP=2 and TP=4 cases in
tests/test_vocab_parallel_logp.py, ensuring CI validates cross-TP bitwise
equality rather than skipping those tests.


docs:
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 8 additions & 0 deletions docs/design/runtime-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ logical type, and the registry selects the first available backend for the curre
4. Cache successfully constructed operator instances.
5. Skip backends that already failed in the current process.

WS2 TP-aware logprob uses the stricter `KernelRegistry.get_logprob_op(contract)` path. In
addition to platform priority, this path requires a backend capability descriptor and checks
the requested role, dtype, TP/CP layout, padded-vs-real vocab masking, inactive-token
support, vocab-domain LSE export, and deterministic TP merge semantics. Incompatible
candidates produce explicit rejection reasons and are never used as an undeclared fallback.
The contract objects and their normative reduction semantics are documented in
`rl_engine.kernels.logprob_contract`.

## LogP Priority

| Platform | Priority |
Expand Down
29 changes: 29 additions & 0 deletions docs/operators/batch-invariant-logp.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ CUDA priority list when the extension exposes `_C.batch_invariant_logp_sm90`
(built with `KERNEL_ALIGN_FORCE_SM90=1`) on an SM90 device. On any other build
or device, dispatch is unchanged (Triton -> PyTorch).

## Tensor Parallel

`VocabParallelLogprobOp`
(`rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`)
**TP=1, TP=2, and TP=4 produce bit-identical results.**

1. Split the padded vocabulary into `num_vocab_tiles` fixed tiles.
2. Each rank computes fp32 `(max, sumexp)` for the tiles it owns. Every tile
is reduced as the same contiguous `[n, tile]` shape, on any rank.
3. All tile partials are shared with `all_gather`. The collective only moves
bytes; it never does math, so it cannot round anything.
4. Every rank merges all tiles in the same fixed order, over the same
`[n, num_vocab_tiles]` shape. `LSE = M + log(sum(s_t * exp(m_t - M)))`.
5. The target logit is copied from the rank that owns it (never summed).
6. `logp = target_logit - LSE`. Inactive rows become `0.0`.

Comment on lines +57 to +72

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

State the num_vocab_tiles precondition with the bit-identical claim.

Line 61 asserts that TP=1, TP=2, and TP=4 produce bit-identical results. The implementation provides that guarantee only while num_vocab_tiles is held fixed across the compared TP degrees and agreed on by every rank. The module docstring of vocab_parallel_logp.py states this explicitly, and _preflight_cross_rank_agreement enforces the cross-rank half. A reader of this section alone can change num_vocab_tiles between two runs and lose the property. Also add the shard-alignment requirement, since _tile_size rejects non-tile-aligned bounds.

Lines 59 to 61 also render as a single paragraph, so the operator name, the file path, and the claim run together without punctuation.

📝 Proposed wording
 ## Tensor Parallel
 
-`VocabParallelLogprobOp`
-(`rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`)
-**TP=1, TP=2, and TP=4 produce bit-identical results.**
+`VocabParallelLogprobOp`
+(`rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`) computes the
+selected-token logprob across vocab-parallel TP ranks.
+**TP=1, TP=2, and TP=4 produce bit-identical results**, provided
+`num_vocab_tiles` is the same value at every TP degree and on every rank, and
+every shard boundary is tile-aligned. Both conditions fail loudly.
📝 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
## Tensor Parallel
`VocabParallelLogprobOp`
(`rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`)
**TP=1, TP=2, and TP=4 produce bit-identical results.**
1. Split the padded vocabulary into `num_vocab_tiles` fixed tiles.
2. Each rank computes fp32 `(max, sumexp)` for the tiles it owns. Every tile
is reduced as the same contiguous `[n, tile]` shape, on any rank.
3. All tile partials are shared with `all_gather`. The collective only moves
bytes; it never does math, so it cannot round anything.
4. Every rank merges all tiles in the same fixed order, over the same
`[n, num_vocab_tiles]` shape. `LSE = M + log(sum(s_t * exp(m_t - M)))`.
5. The target logit is copied from the rank that owns it (never summed).
6. `logp = target_logit - LSE`. Inactive rows become `0.0`.
## Tensor Parallel
`VocabParallelLogprobOp`
(`rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`) computes the
selected-token logprob across vocab-parallel TP ranks.
**TP=1, TP=2, and TP=4 produce bit-identical results**, provided
`num_vocab_tiles` is the same value at every TP degree and on every rank, and
every shard boundary is tile-aligned. Both conditions fail loudly.
1. Split the padded vocabulary into `num_vocab_tiles` fixed tiles.
2. Each rank computes fp32 `(max, sumexp)` for the tiles it owns. Every tile
is reduced as the same contiguous `[n, tile]` shape, on any rank.
3. All tile partials are shared with `all_gather`. The collective only moves
bytes; it never does math, so it cannot round anything.
4. Every rank merges all tiles in the same fixed order, over the same
`[n, num_vocab_tiles]` shape. `LSE = M + log(sum(s_t * exp(m_t - M)))`.
5. The target logit is copied from the rank that owns it (never summed).
6. `logp = target_logit - LSE`. Inactive rows become `0.0`.
🤖 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 `@docs/operators/batch-invariant-logp.md` around lines 57 - 72, Update the
Tensor Parallel documentation around VocabParallelLogprobOp to state that the
bit-identical TP=1/2/4 guarantee requires the same num_vocab_tiles across
compared runs, agreement on that value across ranks, and tile-aligned shard
bounds. Also add punctuation or line breaks so the operator name, source path,
and claim are clearly separated.

Usage goes through the contract-aware entry point:

```python
from rl_engine.kernels.registry import kernel_registry

result = kernel_registry.get_logprob_op(contract) # LogprobContract from
op = result.op # rl_engine.kernels.logprob_contract
logp, lse = op(local_logits, target_ids, contract=contract, tp_group=tp_group)
```

## Benchmarks

`benchmarks/benchmark_batch_invariant_logp.py` compares Native, Triton, and the
Expand Down Expand Up @@ -224,3 +250,6 @@ WSL/Linux with CUDA.
- `rl_engine/kernels/registry.py`
- `tests/test_batch_invariant_logp.py`
- `benchmarks/benchmark_batch_invariant_logp.py`
- `rl_engine/kernels/ops/pytorch/loss/vocab_parallel_logp.py`
- `rl_engine/kernels/logprob_contract.py`
- `tests/test_vocab_parallel_logp.py`
Loading
Loading