Skip to content

[draft][WS1][kernels] Batch-invariant logprob Ascend C Kernel - #297

Merged
Flink-ddd merged 4 commits into
RL-Align:testfrom
erfgss:feat/ascendc_
Aug 20, 2026
Merged

[draft][WS1][kernels] Batch-invariant logprob Ascend C Kernel#297
Flink-ddd merged 4 commits into
RL-Align:testfrom
erfgss:feat/ascendc_

Conversation

@erfgss

@erfgss erfgss commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an Ascend C (CANN) backend for the batch-invariant selected-token
log-probability operator introduced in #199 and extended to SM90 CUDA in #204.
The operator computes, from already materialized logits:

logp[t] = logits[t, target_ids[t]] - logsumexp(logits[t, :])

The kernel streams each vocab row through UB in fixed 4096-element tiles with a
two-pass (max, then sum-exp) fixed-order fp32 reduction, so the per-row result
depends only on the row itself — never on batch size or layout. Each row is
processed end-to-end by exactly one AI core block, and outputs are staged in UB
and written with DataCopyPad (scalar GM SetValue/GetValue are avoided per
known hardware pitfalls). It is registered as the top-priority backend on the
new npu platform key and falls back cleanly to the PyTorch native op.

Path Status
Ascend C forward (csrc/ascend/batch_invariant_logp_ascend.asc) Two-pass streaming reduction, bf16 + fp32, one block per row. New.
Python op (BatchInvariantLogpAscendOp) Autograd wrapper; Ascend C forward + PyTorch-formula backward reusing the forward-saved lse. New.
Extension (rl_engine._C_npu) New bisheng-built (-x asc) extension, gated on KERNEL_ALIGN_FORCE_ASCEND=1; --npu-arch defaults to dav-2201, override via KERNEL_ALIGN_ASCEND_ARCH. New.
Platform DeviceContext now detects torch_npu / torch.npu; registry gains the "npu" platform key. New.
Registry batch_invariant_logp on NPU: Ascend -> PyTorch. Ascend instantiation failure (extension not built) degrades to native.
Fallback Non-bf16/fp32 or non-NPU tensors -> PyTorch native (Triton rejects non-CUDA devices).
Benchmark benchmarks/benchmark_batch_invariant_logp.py dispatches timing/VRAM through torch.npu and adds an ascend column.

Implementation

  • New kernel + binding: csrc/ascend/batch_invariant_logp_ascend.asc
    (kernel class, __global__ __vector__ entries, host entrypoint, pybind).
  • Build: setup.py (AscendBuildExtension invoking bisheng, following
    the official torch_npu cpp_extension_asc pattern), envs.py
    (KERNEL_ALIGN_FORCE_ASCEND, KERNEL_ALIGN_ASCEND_ARCH), rl_engine/_C_npu.pyi.
  • Python op: rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py,
    interface aligned with BatchInvariantLogpSM90Op.
  • Platform/registry: rl_engine/platforms/device.py,
    rl_engine/kernels/registry.py.
  • Tests/docs/benchmark: tests/test_batch_invariant_logp.py,
    rl_engine/kernels/gtest/operator_specs.py,
    docs/operators/batch-invariant-logp.md,
    benchmarks/benchmark_batch_invariant_logp.py.

Validation environment

Item Value
NPU Ascend (dav_c220, CANN dav-2201 target)
CANN toolkit 9.1.0
PyTorch / torch_npu 2.7.1 / matching torch_npu
Python 3.11 (aarch64)
Build KERNEL_ALIGN_FORCE_ASCEND=1 pip install -e . --no-build-isolation

Correctness / Tests

python -m pytest tests/test_batch_invariant_logp.py -k Ascend -q -rs
  • TestAscendCorrectness (fp32/bf16 vs log_softmax + gather, large vocab,
    single-token, 3-D, vs native op): 6/6 passed on device.
  • Intra-core pipeline sync uses per-pipe SetFlag/WaitFlag events;
    AscendC::SyncAll() (cross-core barrier) is deliberately avoided because it
    deadlocks when launched blocks exceed resident cores.

WIP: TestAscendBatchInvariance / TestAscendBackward /
TestAscendIgnoreIndex / TestAscendFallback are included but still being
re-verified on device after the sync fix; will update before undrafting.

CPU-only regression (no NPU): tests/test_batch_invariant_logp.py,
tests/test_kernel_registry.py, rl_engine/tests/test_dispatch.py,
tests/test_cpu_hal.py — all pass; Ascend tests skip cleanly.

Files

  • csrc/ascend/batch_invariant_logp_ascend.asc (new)
  • rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py (new)
  • rl_engine/_C_npu.pyi (new)
  • csrc/ascend/, rl_engine/kernels/ops/ascend/ package init files (new)
  • setup.py, envs.py, rl_engine/kernels/registry.py,
    rl_engine/platforms/device.py
  • tests/test_batch_invariant_logp.py,
    rl_engine/kernels/gtest/operator_specs.py,
    docs/operators/batch-invariant-logp.md,
    benchmarks/benchmark_batch_invariant_logp.py


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added Ascend NPU support for batch-invariant selected-token log probabilities.
  * Supports BF16 and FP32 inputs, gradient computation, ignored targets, and automatic fallback for unsupported configurations.
  * Added Ascend device detection, build configuration, and backend dispatch.

* **Performance**
  * Added accelerator-aware benchmarking across CUDA, ROCm, and Ascend, including latency, speedup, and memory reporting.

* **Documentation**
  * Documented Ascend support, configuration, dispatch behavior, supported data types, and fallback rules.

* **Tests**
  * Added Ascend coverage for correctness, batching, gradients, ignored targets, and fallback behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: chenyang <2082464740@qq.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

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

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c874fcc0-8542-4e80-afd5-2dc10a784623

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
📝 Walkthrough

Walkthrough

This PR adds an Ascend CANN implementation for batch-invariant selected-token log probabilities. It integrates NPU detection, custom extension compilation, runtime dispatch, fallback behavior, tests, documentation, and accelerator-aware benchmarking.

Changes

Ascend batch-invariant log-probability

Layer / File(s) Summary
Ascend extension build and kernel
csrc/ascend/batch_invariant_logp_ascend.asc, setup.py, envs.py
Adds fp32 and bf16 Ascend kernels, PyBind registration, environment settings, and a bisheng build path for rl_engine._C_npu.
NPU device and operator dispatch
rl_engine/platforms/device.py, rl_engine/kernels/registry.py, rl_engine/kernels/ops/ascend/..., rl_engine/_C_npu.pyi, rl_engine/kernels/gtest/operator_specs.py
Detects NPU devices, registers Ascend priority dispatch, invokes the compiled extension, validates inputs, computes gradients, and falls back to PyTorch when required.
Ascend validation and operator contract
tests/test_batch_invariant_logp.py, docs/operators/batch-invariant-logp.md
Tests correctness, batch invariance, gradients, ignore-index behavior, unsupported-dtype fallback, and registry dispatch. Documents Ascend support and build behavior.
Accelerator-aware benchmark flow
benchmarks/benchmark_batch_invariant_logp.py
Adds optional Triton loading, accelerator-specific timing and memory APIs, dynamic CUDA/Ascend backend selection, and conditional benchmark reporting.

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

Possibly related PRs

  • RL-Align/RL-Kernel#98: Adds another batch-invariant log-probability backend and related dispatch and benchmark integration.
  • RL-Align/RL-Kernel#199: Introduces the operator and integration points extended here for Ascend.
  • RL-Align/RL-Kernel#204: Extends the same operator across hardware backends, including benchmark, registry, documentation, and test changes.

Suggested labels: needs-gpu-ci

Suggested reviewers: bitborne, inaniloquentee, flink-ddd, kjldefeated

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% 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 identifies the main change: adding an Ascend C kernel for batch-invariant log probabilities.
✨ 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: 9

🧹 Nitpick comments (6)
rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py (1)

30-37: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Cache the fallback operator.

_fallback_op() performs a module import and constructs a new NativeBatchInvariantLogpOp on every call. For fp16 inputs this runs on each forward. Build it once.

♻️ Proposed fix
+_FALLBACK_OP = None
+
+
 def _fallback_op():
     """Portable op for inputs the Ascend forward cannot take.
 
     Triton rejects non-CUDA devices, so on NPU the only fallback is native.
     """
-    from rl_engine.kernels.ops.pytorch.loss.batch_invariant_logp import NativeBatchInvariantLogpOp
-
-    return NativeBatchInvariantLogpOp()
+    global _FALLBACK_OP
+    if _FALLBACK_OP is None:
+        from rl_engine.kernels.ops.pytorch.loss.batch_invariant_logp import (
+            NativeBatchInvariantLogpOp,
+        )
+
+        _FALLBACK_OP = NativeBatchInvariantLogpOp()
+    return _FALLBACK_OP
🤖 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/ascend/loss/batch_invariant_logp.py` around lines 30 -
37, Cache the operator returned by _fallback_op so the
NativeBatchInvariantLogpOp import and construction occur only once, while
preserving the existing fallback behavior for fp16 inputs.
setup.py (2)

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

Anchor the source glob to the setup.py directory.

Path("csrc/ascend") resolves against the current working directory. If a build front-end invokes setup.py from another directory, the glob returns no files and the build raises the "no .asc sources" error. Use a path derived from __file__.

♻️ Proposed fix
-    asc_srcs = sorted(str(p) for p in Path("csrc/ascend").glob("*.asc"))
+    root = Path(__file__).parent.resolve()
+    asc_srcs = sorted(str(p) for p in (root / "csrc" / "ascend").glob("*.asc"))
🤖 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 `@setup.py` around lines 218 - 221, Update the source discovery in the Ascend
extension setup flow around the asc_srcs expression to resolve csrc/ascend
relative to setup.py via __file__, rather than the current working directory.
Preserve the existing sorting, missing-source RuntimeError, and Extension
construction behavior.

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the distutils imports.

The project supports Python 3.10 and later. Python 3.12 removed distutils, and direct setup.py execution imports it before setuptools can install its compatibility shim. Use setuptools.errors.CompileError and shutil.which instead. The setuptools>=64 build requirement provides CompileError.

🤖 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 `@setup.py` around lines 6 - 8, Replace the distutils imports in setup.py:
import CompileError from setuptools.errors and use shutil.which instead of
distutils.spawn.find_executable, while retaining sysconfig and updating any
corresponding call sites to the new executable lookup symbol.
tests/test_batch_invariant_logp.py (1)

1195-1213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the empty batch and for validate=True.

Two reachable paths have no test:

  • An empty batch (logits shaped [0, V]). The kernel computes blockNum = 0 for this input. See the related comment on csrc/ascend/batch_invariant_logp_ascend.asc.
  • validate=True with an out-of-range target. The Ascend wrapper defaults to validate=False, and the kernel returns a silent -lse for out-of-range targets rather than raising.

Do you want me to generate these test cases?

🤖 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_batch_invariant_logp.py` around lines 1195 - 1213, Extend
TestAscendIgnoreIndex with coverage for an empty logits batch shaped [0, _VC]
and matching empty targets, asserting the operation returns an empty output. Add
a validate=True case using an out-of-range target and assert the Ascend
operation raises the expected validation error, while preserving the existing
ignore-index and default validate=False coverage.
csrc/ascend/batch_invariant_logp_ascend.asc (1)

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

The 1-element Log reads seven uninitialized floats.

scalar.SetValue(0, sumExp) initializes only element 0, and AscendC::Log(scalar, scalar, 8) processes elements 0 through 7. Elements 1 through 7 hold whatever remained in UB. Only element 0 is read afterwards, so the result is correct, but the operation computes log on undefined data on every row. Initialize the padding once in Init to keep the behavior defined.

♻️ Proposed fix
         AscendC::LocalTensor<float> scalar = scalarBuf_.Get<float>();
         scalar.SetValue(0, sumExp);
+        for (uint32_t i = 1; i < 8; ++i) {
+            scalar.SetValue(i, 1.0f);
+        }
         AscendC::SetFlag<AscendC::HardEvent::S_V>(eventSV_);  // scalar write -> vector op
🤖 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 `@csrc/ascend/batch_invariant_logp_ascend.asc` around lines 164 - 172,
Initialize all eight elements of scalarBuf_ during Init before any Log
operation, ensuring the padding elements have defined values. Update the scalar
setup around scalarBuf_, scalar, and AscendC::Log so per-row processing only
overwrites element 0 while preserving defined padding for the length-8 vector
operation.
docs/operators/batch-invariant-logp.md (1)

59-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the Ascend validate default and the out-of-range behavior.

Lines 154-157 state that the PyTorch backend validates target ranges by default and that Triton defaults to validate=False. BatchInvariantLogpAscendOp.apply also defaults to validate=False, and the Ascend kernel returns -lse for an out-of-range target instead of raising. Add that to this section so callers know the reference operator and the Ascend operator disagree on invalid input.

🤖 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 59 - 66, Update the
Ascend backend documentation to state that BatchInvariantLogpAscendOp.apply
defaults to validate=False and that the Ascend kernel returns -lse for
out-of-range targets rather than raising. Explicitly contrast this with the
PyTorch reference backend’s default target-range validation so callers
understand the invalid-input behavior difference.
🤖 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 `@benchmarks/benchmark_batch_invariant_logp.py`:
- Line 11: Update the description of NativeBatchInvariantLogpOp to state that it
materializes FP32 intermediate tensors for the explicit row-wise logsumexp
calculation, removing the claim that it materializes a full [N, V] log_softmax
tensor.
- Around line 88-95: Update _maybe_accelerated_op to accept or access device_ctx
and only call/select _maybe_sm90_op when device_ctx.device_type is "cuda", and
only call/select _maybe_ascend_op when it is "npu". Preserve the existing
(label, operator) return values and the (None, None) fallback for unsupported or
unavailable devices.

In `@csrc/ascend/batch_invariant_logp_ascend.asc`:
- Around line 141-143: Update the kernel header comment in
csrc/ascend/batch_invariant_logp_ascend.asc around the target selection in
BatchInvariantLogpAscendOp to state that invalid non-ignore targets remain
unvalidated and produce -lse rather than an error; no kernel logic change is
requested. Update docs/operators/batch-invariant-logp.md around lines 59-66 to
document the Ascend backend’s validate=False default and its -lse behavior for
out-of-range targets.
- Around line 270-285: In the batch setup before stream acquisition and kernel
launch, handle numRows == 0 by returning the already allocated empty logp and
lse tensors. Keep the existing validation and non-empty blockNum/kernel-launch
path unchanged.

In `@rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py`:
- Around line 62-80: Reduce the backward memory usage in the static method
backward by reusing a single [N, V] buffer: transform the logits buffer in place
into probabilities, apply the one-hot target and gradient scaling without
allocating separate probs, onehot, or grad tensors, and zero ignored rows in the
same buffer. Preserve the existing dtype conversion and reshape the final
gradient using tuple concatenation (ctx.lead_shape + (vocab_size,)) to avoid the
RUF005 warning.

In `@rl_engine/kernels/registry.py`:
- Around line 293-300: Update the npu platform map in the registry to include
every operator defined in the cpu map, preserving each CPU backend mapping by
default and overriding only batch_invariant_logp with its Ascend and PyTorch
backends. Ensure get_op resolves rms_norm, attention, grpo_loss, rope, matmul,
silu, swiglu, pack, lm_head, and embedding to their corresponding native
implementations on NPU hosts.

In `@rl_engine/platforms/device.py`:
- Around line 10-16: Make the shared _npu_available helper resilient by catching
Exception around both torch_npu import and torch.npu.is_available(), returning
False on any failure; update rl_engine/platforms/device.py:10-16 accordingly. In
tests/test_batch_invariant_logp.py:64-82, remove the duplicated local helper and
import _npu_available from rl_engine.platforms.device, while widening
_ascend_kernel_available’s handler to except Exception.

In `@setup.py`:
- Around line 256-279: Update the command construction in the extension build
function so include paths and library search paths precede ext.sources, followed
by the required -lascendcl, -ltorch_npu, -ltorch, -ltorch_cpu, -ltorch_python,
and -lc10 options for left-to-right linker resolution. Add runtime rpath entries
for the Ascend and PyTorch library directories when needed to allow _C_npu.so to
import without sourced CANN or torch_npu environments.

In `@tests/test_batch_invariant_logp.py`:
- Around line 64-82: Remove the local _npu_available implementation and import
the shared _npu_available helper from rl_engine.platforms.device. Update
_ascend_kernel_available to use that imported helper, preserving its existing
kernel-extension checks and avoiding duplicate import/error-handling logic.

---

Nitpick comments:
In `@csrc/ascend/batch_invariant_logp_ascend.asc`:
- Around line 164-172: Initialize all eight elements of scalarBuf_ during Init
before any Log operation, ensuring the padding elements have defined values.
Update the scalar setup around scalarBuf_, scalar, and AscendC::Log so per-row
processing only overwrites element 0 while preserving defined padding for the
length-8 vector operation.

In `@docs/operators/batch-invariant-logp.md`:
- Around line 59-66: Update the Ascend backend documentation to state that
BatchInvariantLogpAscendOp.apply defaults to validate=False and that the Ascend
kernel returns -lse for out-of-range targets rather than raising. Explicitly
contrast this with the PyTorch reference backend’s default target-range
validation so callers understand the invalid-input behavior difference.

In `@rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py`:
- Around line 30-37: Cache the operator returned by _fallback_op so the
NativeBatchInvariantLogpOp import and construction occur only once, while
preserving the existing fallback behavior for fp16 inputs.

In `@setup.py`:
- Around line 218-221: Update the source discovery in the Ascend extension setup
flow around the asc_srcs expression to resolve csrc/ascend relative to setup.py
via __file__, rather than the current working directory. Preserve the existing
sorting, missing-source RuntimeError, and Extension construction behavior.
- Around line 6-8: Replace the distutils imports in setup.py: import
CompileError from setuptools.errors and use shutil.which instead of
distutils.spawn.find_executable, while retaining sysconfig and updating any
corresponding call sites to the new executable lookup symbol.

In `@tests/test_batch_invariant_logp.py`:
- Around line 1195-1213: Extend TestAscendIgnoreIndex with coverage for an empty
logits batch shaped [0, _VC] and matching empty targets, asserting the operation
returns an empty output. Add a validate=True case using an out-of-range target
and assert the Ascend operation raises the expected validation error, while
preserving the existing ignore-index and default validate=False coverage.
🪄 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: b87b57d4-6126-4a63-9086-6fff9b9aed60

📥 Commits

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

📒 Files selected for processing (13)
  • benchmarks/benchmark_batch_invariant_logp.py
  • csrc/ascend/batch_invariant_logp_ascend.asc
  • docs/operators/batch-invariant-logp.md
  • envs.py
  • rl_engine/_C_npu.pyi
  • rl_engine/kernels/gtest/operator_specs.py
  • rl_engine/kernels/ops/ascend/__init__.py
  • rl_engine/kernels/ops/ascend/loss/__init__.py
  • rl_engine/kernels/ops/ascend/loss/batch_invariant_logp.py
  • rl_engine/kernels/registry.py
  • rl_engine/platforms/device.py
  • setup.py
  • tests/test_batch_invariant_logp.py

(batch-invariant). The comparison here is latency and peak device memory across
a vocab sweep:

- Native materializes ``log_softmax`` over the full ``[N, V]`` tensor.

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

Correct the native-backend description.

NativeBatchInvariantLogpOp does not materialize log_softmax. It materializes FP32 intermediate tensors for the explicit row-wise logsumexp calculation. Update this line to describe that implementation.

🤖 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 `@benchmarks/benchmark_batch_invariant_logp.py` at line 11, Update the
description of NativeBatchInvariantLogpOp to state that it materializes FP32
intermediate tensors for the explicit row-wise logsumexp calculation, removing
the claim that it materializes a full [N, V] log_softmax tensor.

Comment on lines +88 to +95
def _maybe_accelerated_op():
"""(label, op) for the fastest hardware kernel on this host, else (None, None)."""
sm90_op = _maybe_sm90_op()
if sm90_op is not None:
return "cuda", sm90_op
ascend_op = _maybe_ascend_op()
if ascend_op is not None:
return "ascend", ascend_op

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Select the accelerated operator for the active device.

Lines 90-92 can select BatchInvariantLogpSM90Op on a host that has SM90 CUDA and an active NPU device. run_benchmark then creates NPU tensors and passes them to the CUDA operator. Restrict SM90 selection to device_ctx.device_type == "cuda" and Ascend selection to device_ctx.device_type == "npu".

Proposed fix
 def _maybe_accelerated_op():
-    sm90_op = _maybe_sm90_op()
-    if sm90_op is not None:
-        return "cuda", sm90_op
-    ascend_op = _maybe_ascend_op()
-    if ascend_op is not None:
-        return "ascend", ascend_op
+    if device_ctx.device_type == "cuda":
+        sm90_op = _maybe_sm90_op()
+        if sm90_op is not None:
+            return "cuda", sm90_op
+    if device_ctx.device_type == "npu":
+        ascend_op = _maybe_ascend_op()
+        if ascend_op is not None:
+            return "ascend", ascend_op
     return None, None
🤖 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 `@benchmarks/benchmark_batch_invariant_logp.py` around lines 88 - 95, Update
_maybe_accelerated_op to accept or access device_ctx and only call/select
_maybe_sm90_op when device_ctx.device_type is "cuda", and only call/select
_maybe_ascend_op when it is "npu". Preserve the existing (label, operator)
return values and the (None, None) fallback for unsupported or unavailable
devices.

Comment on lines +141 to +143
if (valid && target >= start && target < start + count) {
selected = fLocal.GetValue(static_cast<uint32_t>(target - start));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The Ascend path skips target-range validation by default and does not report invalid targets. BatchInvariantLogpAscendOp.apply defaults to validate=False, and the kernel has no device-side range check. For a target outside [0, vocabSize) that is not ignoreIndex, no tile matches the condition on line 141, selected keeps 0.0f, and the row returns -lse. The native reference raises ValueError for the same input.

  • csrc/ascend/batch_invariant_logp_ascend.asc#L141-L143: state the out-of-range contract in the kernel header comment, or clamp the result to match the reference.
  • docs/operators/batch-invariant-logp.md#L59-L66: document that the Ascend backend defaults to validate=False and that an out-of-range target yields -lse instead of an error.
📍 Affects 2 files
  • csrc/ascend/batch_invariant_logp_ascend.asc#L141-L143 (this comment)
  • docs/operators/batch-invariant-logp.md#L59-L66
🤖 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 `@csrc/ascend/batch_invariant_logp_ascend.asc` around lines 141 - 143, Update
the kernel header comment in csrc/ascend/batch_invariant_logp_ascend.asc around
the target selection in BatchInvariantLogpAscendOp to state that invalid
non-ignore targets remain unvalidated and produce -lse rather than an error; no
kernel logic change is requested. Update docs/operators/batch-invariant-logp.md
around lines 59-66 to document the Ascend backend’s validate=False default and
its -lse behavior for out-of-range targets.

Comment on lines +270 to +285
TORCH_CHECK(logits.size(-1) > 0, "vocab size must be positive");
TORCH_CHECK(target.is_privateuseone(), "target must be on the same NPU device as logits");
TORCH_CHECK(target.numel() == logits.size(0), "target must have one entry per row");

const int64_t numRows = logits.size(0);
const int64_t vocabSize = logits.size(1);

torch::Tensor targetI32 = target.to(torch::kInt32).contiguous();
torch::Tensor logp = at::empty({numRows}, logits.options().dtype(at::kFloat));
torch::Tensor lse = at::empty({numRows}, logits.options().dtype(at::kFloat));

// stream(true): flush the task queue before launch so the kernel cannot
// overtake earlier NPU work; outputs were allocated with at::empty (no
// queued initializer) for the same reason.
auto aclStream = c10_npu::getCurrentNPUStream().stream(true);
const uint32_t blockNum = static_cast<uint32_t>(std::min(numRows, MAX_BLOCKS));

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 | ⚡ Quick win

Guard the empty-batch case before the launch.

TORCH_CHECK validates the vocab dimension but not the row dimension. If logits.size(0) == 0, then blockNum becomes 0 and the kernel launches with a zero block count. Return the empty outputs before the launch.

🛡️ Proposed fix
     torch::Tensor logp = at::empty({numRows}, logits.options().dtype(at::kFloat));
     torch::Tensor lse = at::empty({numRows}, logits.options().dtype(at::kFloat));
+
+    if (numRows == 0) {
+        return {logp, lse};
+    }
📝 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
TORCH_CHECK(logits.size(-1) > 0, "vocab size must be positive");
TORCH_CHECK(target.is_privateuseone(), "target must be on the same NPU device as logits");
TORCH_CHECK(target.numel() == logits.size(0), "target must have one entry per row");
const int64_t numRows = logits.size(0);
const int64_t vocabSize = logits.size(1);
torch::Tensor targetI32 = target.to(torch::kInt32).contiguous();
torch::Tensor logp = at::empty({numRows}, logits.options().dtype(at::kFloat));
torch::Tensor lse = at::empty({numRows}, logits.options().dtype(at::kFloat));
// stream(true): flush the task queue before launch so the kernel cannot
// overtake earlier NPU work; outputs were allocated with at::empty (no
// queued initializer) for the same reason.
auto aclStream = c10_npu::getCurrentNPUStream().stream(true);
const uint32_t blockNum = static_cast<uint32_t>(std::min(numRows, MAX_BLOCKS));
TORCH_CHECK(logits.size(-1) > 0, "vocab size must be positive");
TORCH_CHECK(target.is_privateuseone(), "target must be on the same NPU device as logits");
TORCH_CHECK(target.numel() == logits.size(0), "target must have one entry per row");
const int64_t numRows = logits.size(0);
const int64_t vocabSize = logits.size(1);
torch::Tensor targetI32 = target.to(torch::kInt32).contiguous();
torch::Tensor logp = at::empty({numRows}, logits.options().dtype(at::kFloat));
torch::Tensor lse = at::empty({numRows}, logits.options().dtype(at::kFloat));
if (numRows == 0) {
return {logp, lse};
}
// stream(true): flush the task queue before launch so the kernel cannot
// overtake earlier NPU work; outputs were allocated with at::empty (no
// queued initializer) for the same reason.
auto aclStream = c10_npu::getCurrentNPUStream().stream(true);
const uint32_t blockNum = static_cast<uint32_t>(std::min(numRows, MAX_BLOCKS));
🤖 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 `@csrc/ascend/batch_invariant_logp_ascend.asc` around lines 270 - 285, In the
batch setup before stream acquisition and kernel launch, handle numRows == 0 by
returning the already allocated empty logp and lse tensors. Keep the existing
validation and non-empty blockNum/kernel-launch path unchanged.

Comment on lines +62 to +80
@staticmethod
def backward(ctx, grad_output):
logits_2d, target_1d, lse = ctx.saved_tensors
ignore_index = ctx.ignore_index
vocab_size = ctx.vocab_size

grad_flat = grad_output.reshape(-1).contiguous().to(torch.float32)

valid = target_1d != ignore_index
safe_target = torch.where(valid, target_1d, torch.zeros_like(target_1d))
probs = torch.exp(logits_2d.float() - lse.unsqueeze(1))
onehot = torch.zeros_like(probs)
onehot.scatter_(1, safe_target.unsqueeze(1), 1.0)
grad = grad_flat.unsqueeze(1) * (onehot - probs)
grad = torch.where(valid.unsqueeze(1), grad, torch.zeros_like(grad))
grad_logits = grad.to(logits_2d.dtype)

grad_logits = grad_logits.reshape(ctx.lead_shape + (vocab_size,))
return grad_logits, None, None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

The backward pass allocates three full [N, V] fp32 tensors.

probs, onehot, and grad are each [N, V] in fp32. For a 8192-token batch with a 128k vocabulary that is roughly 12 GB of peak device memory for a single backward call, on top of the saved logits_2d. The stated goal is to avoid materializing extra [N, V] buffers. You can produce the same result with one buffer.

♻️ Proposed lower-memory backward
     `@staticmethod`
     def backward(ctx, grad_output):
         logits_2d, target_1d, lse = ctx.saved_tensors
         ignore_index = ctx.ignore_index
         vocab_size = ctx.vocab_size
 
         grad_flat = grad_output.reshape(-1).contiguous().to(torch.float32)
 
         valid = target_1d != ignore_index
         safe_target = torch.where(valid, target_1d, torch.zeros_like(target_1d))
-        probs = torch.exp(logits_2d.float() - lse.unsqueeze(1))
-        onehot = torch.zeros_like(probs)
-        onehot.scatter_(1, safe_target.unsqueeze(1), 1.0)
-        grad = grad_flat.unsqueeze(1) * (onehot - probs)
-        grad = torch.where(valid.unsqueeze(1), grad, torch.zeros_like(grad))
-        grad_logits = grad.to(logits_2d.dtype)
+        # grad = g * (onehot - softmax); built in a single [N, V] buffer.
+        grad = torch.exp(logits_2d.float() - lse.unsqueeze(1))
+        grad.neg_()
+        grad.scatter_add_(1, safe_target.unsqueeze(1), torch.ones_like(lse).unsqueeze(1))
+        grad.mul_(torch.where(valid, grad_flat, torch.zeros_like(grad_flat)).unsqueeze(1))
+        grad_logits = grad.to(logits_2d.dtype)
 
-        grad_logits = grad_logits.reshape(ctx.lead_shape + (vocab_size,))
+        grad_logits = grad_logits.reshape((*ctx.lead_shape, vocab_size))
         return grad_logits, None, None

The reshape change also resolves the Ruff RUF005 hint on line 79.

📝 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
@staticmethod
def backward(ctx, grad_output):
logits_2d, target_1d, lse = ctx.saved_tensors
ignore_index = ctx.ignore_index
vocab_size = ctx.vocab_size
grad_flat = grad_output.reshape(-1).contiguous().to(torch.float32)
valid = target_1d != ignore_index
safe_target = torch.where(valid, target_1d, torch.zeros_like(target_1d))
probs = torch.exp(logits_2d.float() - lse.unsqueeze(1))
onehot = torch.zeros_like(probs)
onehot.scatter_(1, safe_target.unsqueeze(1), 1.0)
grad = grad_flat.unsqueeze(1) * (onehot - probs)
grad = torch.where(valid.unsqueeze(1), grad, torch.zeros_like(grad))
grad_logits = grad.to(logits_2d.dtype)
grad_logits = grad_logits.reshape(ctx.lead_shape + (vocab_size,))
return grad_logits, None, None
@staticmethod
def backward(ctx, grad_output):
logits_2d, target_1d, lse = ctx.saved_tensors
ignore_index = ctx.ignore_index
vocab_size = ctx.vocab_size
grad_flat = grad_output.reshape(-1).contiguous().to(torch.float32)
valid = target_1d != ignore_index
safe_target = torch.where(valid, target_1d, torch.zeros_like(target_1d))
# grad = g * (onehot - softmax); built in a single [N, V] buffer.
grad = torch.exp(logits_2d.float() - lse.unsqueeze(1))
grad.neg_()
grad.scatter_add_(1, safe_target.unsqueeze(1), torch.ones_like(lse).unsqueeze(1))
grad.mul_(torch.where(valid, grad_flat, torch.zeros_like(grad_flat)).unsqueeze(1))
grad_logits = grad.to(logits_2d.dtype)
grad_logits = grad_logits.reshape((*ctx.lead_shape, vocab_size))
return grad_logits, None, None
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 79-79: Consider (*ctx.lead_shape, vocab_size) instead of concatenation

Replace with (*ctx.lead_shape, vocab_size)

(RUF005)

🤖 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/ascend/loss/batch_invariant_logp.py` around lines 62 -
80, Reduce the backward memory usage in the static method backward by reusing a
single [N, V] buffer: transform the logits buffer in place into probabilities,
apply the one-hot target and gradient scaling without allocating separate probs,
onehot, or grad tensors, and zero ignored rows in the same buffer. Preserve the
existing dtype conversion and reshape the final gradient using tuple
concatenation (ctx.lead_shape + (vocab_size,)) to avoid the RUF005 warning.

Source: Linters/SAST tools

Comment thread rl_engine/kernels/registry.py Outdated
Comment on lines +293 to +300
# Ascend NPU: op types without an entry fall back to PYTORCH_NATIVE
# (see get_op's default), so only Ascend-accelerated ops are listed.
"npu": {
"batch_invariant_logp": [
OpBackend.ASCEND_BATCH_INVARIANT_LOGP,
OpBackend.PYTORCH_BATCH_INVARIANT_LOGP,
],
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

The npu platform map breaks every operator except batch_invariant_logp.

get_op on line 392 uses [OpBackend.PYTORCH_NATIVE] as the default, and PYTORCH_NATIVE maps to rl_engine.kernels.ops.pytorch.loss.logp.NativeLogpOp. It is not a generic fallback. The cpu map lists every operator explicitly for that reason.

Before this change, an Ascend host reported device_ctx.device_type == "cpu" and resolved through the cpu map. After this change, line 421 routes it to npu, and kernel_registry.get_op("rms_norm") now returns NativeLogpOp instead of NativeRMSNormOp. The same applies to attention, grpo_loss, rope, matmul, silu, swiglu, pack, lm_head, and embedding. Each of these fails at call time with a signature or shape error.

Derive the npu map from the cpu map so only batch_invariant_logp differs.

🐛 Proposed fix
-            # Ascend NPU: op types without an entry fall back to PYTORCH_NATIVE
-            # (see get_op's default), so only Ascend-accelerated ops are listed.
-            "npu": {
-                "batch_invariant_logp": [
-                    OpBackend.ASCEND_BATCH_INVARIANT_LOGP,
-                    OpBackend.PYTORCH_BATCH_INVARIANT_LOGP,
-                ],
-            },
         }
+        # Ascend NPU: start from the portable PyTorch map, then override the
+        # ops that have an Ascend kernel. get_op's default is NativeLogpOp,
+        # not a generic fallback, so every op needs an explicit entry.
+        self._priority_map["npu"] = {
+            **self._priority_map["cpu"],
+            "batch_invariant_logp": [
+                OpBackend.ASCEND_BATCH_INVARIANT_LOGP,
+                OpBackend.PYTORCH_BATCH_INVARIANT_LOGP,
+            ],
+        }
📝 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
# Ascend NPU: op types without an entry fall back to PYTORCH_NATIVE
# (see get_op's default), so only Ascend-accelerated ops are listed.
"npu": {
"batch_invariant_logp": [
OpBackend.ASCEND_BATCH_INVARIANT_LOGP,
OpBackend.PYTORCH_BATCH_INVARIANT_LOGP,
],
},
},
}
# Ascend NPU: start from the portable PyTorch map, then override the
# ops that have an Ascend kernel. get_op's default is NativeLogpOp,
# not a generic fallback, so every op needs an explicit entry.
self._priority_map["npu"] = {
**self._priority_map["cpu"],
"batch_invariant_logp": [
OpBackend.ASCEND_BATCH_INVARIANT_LOGP,
OpBackend.PYTORCH_BATCH_INVARIANT_LOGP,
],
}
🤖 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/registry.py` around lines 293 - 300, Update the npu
platform map in the registry to include every operator defined in the cpu map,
preserving each CPU backend mapping by default and overriding only
batch_invariant_logp with its Ascend and PyTorch backends. Ensure get_op
resolves rms_norm, attention, grpo_loss, rope, matmul, silu, swiglu, pack,
lm_head, and embedding to their corresponding native implementations on NPU
hosts.

Comment thread rl_engine/platforms/device.py Outdated
Comment on lines +10 to +16
def _npu_available() -> bool:
"""torch.npu only exists after torch_npu is imported; probe defensively."""
try:
import torch_npu # noqa: F401
except ImportError:
return False
return hasattr(torch, "npu") and torch.npu.is_available()

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 | ⚡ Quick win

One _npu_available helper, duplicated, with an over-narrow except ImportError. Both copies assume import torch_npu fails only with ImportError. A partially installed CANN stack raises OSError for a missing libascendcl.so and RuntimeError for a missing driver, and torch.npu.is_available() can raise as well. The rl_engine/platforms/device.py copy runs at package import time, so the failure aborts import rl_engine; the test copy aborts pytest collection.

  • rl_engine/platforms/device.py#L10-L16: widen the handler to except Exception and move the torch.npu.is_available() call inside the try block.
  • tests/test_batch_invariant_logp.py#L64-L82: delete the local copy and import _npu_available from rl_engine.platforms.device; widen the _ascend_kernel_available handler to except Exception.
📍 Affects 2 files
  • rl_engine/platforms/device.py#L10-L16 (this comment)
  • tests/test_batch_invariant_logp.py#L64-L82
🤖 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/platforms/device.py` around lines 10 - 16, Make the shared
_npu_available helper resilient by catching Exception around both torch_npu
import and torch.npu.is_available(), returning False on any failure; update
rl_engine/platforms/device.py:10-16 accordingly. In
tests/test_batch_invariant_logp.py:64-82, remove the duplicated local helper and
import _npu_available from rl_engine.platforms.device, while widening
_ascend_kernel_available’s handler to except Exception.

Comment thread setup.py
Comment on lines +256 to +279
cmd = [
"bisheng",
"-x",
"asc",
f"--npu-arch={soc}",
"-shared",
"-fPIC",
"-std=c++17",
"-O2",
f"-D_GLIBCXX_USE_CXX11_ABI={abi_value}",
f"-DTORCH_EXTENSION_NAME={module_name}",
"-lascendcl",
"-ltorch_npu",
"-ltorch",
"-ltorch_cpu",
"-ltorch_python",
"-lc10",
*ext.sources,
"-o",
ext_fullpath,
]
cmd += [f"-I{d}" for d in include_dirs if d]
cmd += [f"-L{d}" for d in lib_dirs if d]
return cmd

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how the existing CUDA extension handles library dirs and rpath, for consistency.
rg -n 'rpath|library_dirs|extra_link_args|torch_lib_dir' setup.py

Repository: RL-Align/RL-Kernel

Length of output: 659


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- setup.py outline ---'
ast-grep outline setup.py --lang python || true
printf '%s\n' '--- relevant setup.py sections ---'
sed -n '35,90p' setup.py
sed -n '130,205p' setup.py
sed -n '230,290p' setup.py
printf '%s\n' '--- all relevant symbols and link flags ---'
rg -n -C 3 'def .*build|build_ext|include_dirs|library_dirs|extra_link_args|bisheng|asc|lib_dirs|ext\.sources' setup.py

Repository: RL-Align/RL-Kernel

Length of output: 13362


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Ascend build references ---'
rg -n -i -C 3 'bisheng|KERNEL_ALIGN_FORCE_ASCEND|ASCEND_HOME_PATH|torch_npu|rpath|LD_LIBRARY_PATH|libascendcl|libtorch_npu' --glob '!setup.py' .
printf '%s\n' '--- source and extension declarations ---'
rg -n -C 2 'language="asc"|\.asc|Extension\(|_bisheng_compile_cmd|build_extension' setup.py csrc README.md docs 2>/dev/null || true
printf '%s\n' '--- argument-order verifier ---'
python3 - <<'PY'
from pathlib import Path
import ast

tree = ast.parse(Path("setup.py").read_text())
fn = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "_bisheng_compile_cmd")
cmd_assign = next(n for n in ast.walk(fn) if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "cmd" for t in n.targets))
items = cmd_assign.value.elts
for i, item in enumerate(items):
    if isinstance(item, ast.Starred):
        label = "STARRED " + ast.unparse(item.value)
    elif isinstance(item, ast.Constant):
        label = repr(item.value)
    else:
        label = ast.unparse(item)
    print(f"{i:02d}: {label}")
PY

Repository: RL-Align/RL-Kernel

Length of output: 12074


🌐 Web query:

Bisheng compiler command line link order -l source files Ascend C

💡 Result:

When using the BiSheng compiler for Ascend C development, the command-line compilation and linking order generally follows conventions similar to standard compilers like g++ [1][2]. 1. Compilation of Individual Units: You typically compile source files (.cpp,.asc) into object files (.o) before linking them. If you are using the separate compilation mode (e.g., when making cross-file device code calls), you must use the -dc option for all.asc files [3]. Example: bisheng -dc add_compute.asc -o add_compute.o --npu-arch=dav-2201 bisheng -dc add_kernel.asc -o add_kernel.o --npu-arch=dav-2201 bisheng -c main.cpp -o main.o -I${ASCEND_HOME_PATH}/include 2. Linking Order: After generating the object files, you link them together to produce the final executable. The order in which you pass the object files and library flags matters; object files containing device code or dependencies should generally be placed before the host-side code or where they are referenced [4][3]. Example: bisheng add_compute.o add_kernel.o main.o -o demo 3. Unified Compilation: You can also compile and link in a single command, where the order of source files should reflect the dependencies (e.g., placing kernel source files before the main host code) [4]. Example: bisheng main.cpp add_kernel.asc -o demo --npu-arch=dav-2201 -I${ASCEND_HOME_PATH}/include Key considerations for Ascend C compilation: - --npu-arch: This flag is required to specify the AI processor architecture (e.g., dav-2201) [4][1][2]. - -x asc: Use this flag when compiling.cpp or.c files that contain Ascend C code [1][2]. - Default Linking: The compiler handles default libraries, but if you are linking Ascend C-generated static libraries using a different compiler (like g++), you may need to manually link the default libraries [1][2].

Citations:


🌐 Web query:

torch_npu cpp_extension_asc bisheng rpath library_dirs link flags

💡 Result:

In the context of developing C++ extensions for torch_npu and utilizing the BiSheng compiler for Ascend-based projects, configuration is typically handled through setuptools (setup.py) or manual compiler commands [1][2][3]. Library Paths and Linking When building extensions, you must ensure that the compiler can locate the necessary torch_npu, PyTorch, and CANN/ACL libraries [1][4][2]. 1. Library Paths (library_dirs): You typically need to provide paths to the Python library directory, the PyTorch lib directory, and the torch_npu lib directory [1][2]. These are often retrieved dynamically using sysconfig and the installation paths of the installed packages [1][2]. 2. Link Flags (extra_link_args): Linking requires specifying the libraries to link against, such as -ltorch_npu, -ltorch, and -lc10 [1][2][3]. When using shared libraries, adding RPATH settings is common to ensure the runtime can find these dependencies without explicit environment variable configuration [5]. A standard practice is to use -Wl,-rpath,$ORIGIN/lib or relative paths to the library directory [5]. BiSheng Compiler Integration When integrating the BiSheng compiler (often invoked as the 'bisheng' command), projects frequently implement a custom build_ext class to replace or extend the default setuptools behavior [2][3]. - Compiler Invocation: The build process checks for the existence of the bisheng executable [2][3]. - Compilation Commands: Instead of standard setuptools flags, you may manually construct a command list for bisheng, including arguments such as -shared, -fPIC, -std=c++17, -I for includes, and -L for library paths [2][3]. - NPU-Specific Flags: When compiling, you may need architecture-specific flags like --npu-arch=dav-2201 [2][3]. Reference Implementation Pattern A common pattern seen in Ascend-optimized projects is: - Discovering dependencies: Use a helper function (often named get_dependency_paths) to locate torch, torch_npu, and Python headers/libs [1][2]. - Constructing the command: Iterate through collected include and library directories to append -I and -L flags [2][3]. - Building: Pass the constructed command to the subprocess or self.spawn method within a custom build_ext class [2][3]. For official guidance, refer to the Ascend Extension for PyTorch documentation, which provides details on environment setup and compiler optimizations [6][7]. [1][4][8][2][5][3][7]

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- runtime library-path setup ---'
sed -n '300,365p' rl_engine/executors/deepspeed_trainer.py
printf '%s\n' '--- all environment path mutations ---'
rg -n -C 4 '_prepend_env_path|ASCEND|LD_LIBRARY_PATH|PATH' rl_engine setup.py
printf '%s\n' '--- repository tests for the Bisheng command ---'
rg -n -i -C 3 'bisheng_compile|AscendBuildExtension|_bisheng|lib_dirs|rpath|link' tests setup.py
printf '%s\n' '--- cited reference snippets, if network access is available ---'
for url in \
  'https://raw.githubusercontent.com/Ascend/pytorch/07a3ec02ec359b432cfd931a8e616e25b9c959dd/setup.py' \
  'https://raw.githubusercontent.com/Ascend/pytorch/v2.12.0-26.1.0/torch_npu/utils/cpp_extension.py'
do
  echo "URL: $url"
  curl -L --max-time 10 -s "$url" | rg -n -C 3 'rpath|library_dirs|extra_link_args|torch_npu|ascendcl' | head -80 || true
done

Repository: RL-Align/RL-Kernel

Length of output: 28686


Place the source files and library search paths before the libraries.

BiSheng uses left-to-right linker resolution. With --as-needed, the current order can discard the -l options before ext.sources introduce references, which can leave _C_npu.so with unresolved symbols.

Add rpath entries for the required Ascend and PyTorch library directories if the extension must import without a sourced CANN or torch_npu runtime environment.

🤖 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 `@setup.py` around lines 256 - 279, Update the command construction in the
extension build function so include paths and library search paths precede
ext.sources, followed by the required -lascendcl, -ltorch_npu, -ltorch,
-ltorch_cpu, -ltorch_python, and -lc10 options for left-to-right linker
resolution. Add runtime rpath entries for the Ascend and PyTorch library
directories when needed to allow _C_npu.so to import without sourced CANN or
torch_npu environments.

Comment thread tests/test_batch_invariant_logp.py Outdated
Comment on lines +64 to +82
def _npu_available() -> bool:
try:
import torch_npu # noqa: F401
except ImportError:
return False
return hasattr(torch, "npu") and torch.npu.is_available()


def _ascend_kernel_available() -> bool:
if not _npu_available():
return False
try:
from rl_engine.kernels.ops.ascend.loss.batch_invariant_logp import (
_NPU_EXT_AVAILABLE,
_C_npu,
)
except ImportError:
return False
return _NPU_EXT_AVAILABLE and hasattr(_C_npu, "batch_invariant_logp_ascend")

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 | 🟠 Major | ⚡ Quick win

Reuse _npu_available from rl_engine.platforms.device.

This function is a byte-for-byte copy of _npu_available in rl_engine/platforms/device.py lines 10-16, including the narrow except ImportError. A broken CANN install raises OSError or RuntimeError from import torch_npu, which aborts pytest collection for the whole file instead of skipping the Ascend tests. Import the shared helper so one fix covers both call sites. See the related comment on rl_engine/platforms/device.py.

♻️ Proposed fix
-def _npu_available() -> bool:
-    try:
-        import torch_npu  # noqa: F401
-    except ImportError:
-        return False
-    return hasattr(torch, "npu") and torch.npu.is_available()
+from rl_engine.platforms.device import _npu_available
 
 
 def _ascend_kernel_available() -> bool:
     if not _npu_available():
         return False
     try:
         from rl_engine.kernels.ops.ascend.loss.batch_invariant_logp import (
             _NPU_EXT_AVAILABLE,
             _C_npu,
         )
-    except ImportError:
+    except Exception:
         return False
     return _NPU_EXT_AVAILABLE and hasattr(_C_npu, "batch_invariant_logp_ascend")
📝 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 _npu_available() -> bool:
try:
import torch_npu # noqa: F401
except ImportError:
return False
return hasattr(torch, "npu") and torch.npu.is_available()
def _ascend_kernel_available() -> bool:
if not _npu_available():
return False
try:
from rl_engine.kernels.ops.ascend.loss.batch_invariant_logp import (
_NPU_EXT_AVAILABLE,
_C_npu,
)
except ImportError:
return False
return _NPU_EXT_AVAILABLE and hasattr(_C_npu, "batch_invariant_logp_ascend")
from rl_engine.platforms.device import _npu_available
def _ascend_kernel_available() -> bool:
if not _npu_available():
return False
try:
from rl_engine.kernels.ops.ascend.loss.batch_invariant_logp import (
_NPU_EXT_AVAILABLE,
_C_npu,
)
except Exception:
return False
return _NPU_EXT_AVAILABLE and hasattr(_C_npu, "batch_invariant_logp_ascend")
🤖 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_batch_invariant_logp.py` around lines 64 - 82, Remove the local
_npu_available implementation and import the shared _npu_available helper from
rl_engine.platforms.device. Update _ascend_kernel_available to use that imported
helper, preserving its existing kernel-extension checks and avoiding duplicate
import/error-handling logic.

@erfgss
erfgss changed the base branch from main to test August 12, 2026 10:49

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

LGTM. I have tested this new kernel on NPU and it works. Please fix CI conflicts, thanks for your contribution!

You can use this command,

python scripts/check_operator.py --op batch_invariant_logp --candidate ascend \
      --device npu --dtype fp32 --batch 4 --seq 8 --vocab 2048 --check-grad

And then the ouptut for fp32 is

INFO 08-13 21:34:55 [RL-Kernel]: Successfully linked to precompiled _C_npu.batch_invariant_logp_ascend kernel.
  /home/ma-user/work/z84450661/rl-kernel/rl_engine/kernels/ops/pytorch/loss/batch_invariant_logp.py:105: UserWarning: Cannot create tensor with interal format while
  allow_internel_format=False, tensor will be created with base format. (Triggered internally at ../torch_npu/csrc/aten/common/TensorFactories.cpp:340.)
    selected_logp = selected_logp.where(valid_mask, torch.zeros_like(selected_logp))
  suite=batch_invariant_logp passed=True pass_rate=1.0000
  candidate=ascend-batch_invariant_logp backend=ascend passed=True pass_rate=1.0000
    case=batch_invariant_logp-torch.float32-4x8x2048 output=0 shape=(4, 8) dtype=torch.float32 max_abs=9.53674316e-07 mean_abs=1.78813934e-07 max_rel=1.45964989e-07
  tol=(atol=1.000e-05, rtol=0.000e+00) passed=True
    case=batch_invariant_logp-torch.float32-4x8x2048 output=1 gradient:logits shape=(4, 8, 2048) dtype=torch.float32 max_abs=1.32247806e-07 mean_abs=1.55981117e-10
  max_rel=1.23174477e-05 tol=(atol=1.000e-05, rtol=0.000e+00) passed=True

The output for bf16 is

 INFO 08-13 21:35:49 [RL-Kernel]: Successfully linked to precompiled _C_npu.batch_invariant_logp_ascend kernel.
  ...(同样的 UserWarning 两行)...
  suite=batch_invariant_logp passed=True pass_rate=1.0000
  candidate=ascend-batch_invariant_logp backend=ascend passed=True pass_rate=1.0000
    case=batch_invariant_logp-torch.bfloat16-4x8x2048 output=0 shape=(4, 8) dtype=torch.float32 max_abs=9.53674316e-07 mean_abs=2.98023224e-08 max_rel=1.10753255e-07
  tol=(atol=5.000e-02, rtol=0.000e+00) passed=True

So that means,
forward: max_abs=9.54e-07 tol=1e-05 ✓
gradient: max_abs=1.32e-07 tol=1e-05 ✓
bf16: passed=True(max_abs=9.54e-07, tol=5e-02)✓

Also the 29 unit test passed in tests/test_op_checks.py.

from __future__ import annotations

import torch

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.

Can you add these two lines to the file,
from typing import Any
and
_C_npu = None → _C_npu: Any = None

to pass a CI check called mypy.

You can check the CI lists and fix them if they are not green marks yet

"cuda-sm90": "rl_engine.kernels.ops.cuda.loss.batch_invariant_logp."
"BatchInvariantLogpSM90Op",
"ascend": "rl_engine.kernels.ops.ascend.loss.batch_invariant_logp."
"BatchInvariantLogpAscendOp",

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.

In order to let the new logprob kernel pass gtest (it is our in-house test to measure precision of a single-card kernel), you need to update the scripts/check_operator.py file with the following 3 content,

inside the _select_device method, you need to add these two lines,

if value == "npu" and not _npu_available():
        raise RuntimeError("--device npu was requested, but no Ascend NPU is available")

inside the parse_args method, you need to add the npu option,

parser.add_argument(
        "--candidate",
        default="pytorch",
        help="Candidate backend to validate, for example pytorch, cuda, cuda-sm90, triton, "
        "ascend.",
    )

i think you also need to add a new method,

def _npu_available() -> bool:
    """torch.npu only exists after torch_npu is imported; probe defensively."""
    try:
        import torch_npu  # noqa: F401
    except ImportError:
        return False
    return hasattr(torch, "npu") and torch.npu.is_available()

@Flink-ddd

Copy link
Copy Markdown
Collaborator

please resolve the CI error, other LGTM.

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

LGTM, Thank you for work!

@Flink-ddd
Flink-ddd merged commit dc79395 into RL-Align:test Aug 20, 2026
6 checks passed
zhangj1an added a commit to zhangj1an/rl-kernel that referenced this pull request Aug 20, 2026
…rministic-attention-test

Resolve conflicts with the merged PR RL-Align#297 (batch-invariant logp Ascend):
- logp kernel/op/tests/docs/benchmark take the upstream reviewed versions;
  the kernel's PYBIND11_MODULE stays in the ops_npu.asc aggregator
- registry keeps the upstream cpu-inheritance for the npu platform and adds
  the attention override on top
- setup.py keeps the recursive .asc glob; device.py keeps the upstream
  broader exception probe
- _C_npu.pyi exposes both operator stubs
@erfgss
erfgss deleted the feat/ascendc_ branch August 30, 2026 08:26
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