Skip to content

[TRTLLM-10947][perf] eagle3: use cudaMemcpy2DAsync custom op for hidden-state capture#14479

Merged
pcicotti merged 2 commits into
NVIDIA:mainfrom
pcicotti:llama_eagle3_memcopy
Jun 1, 2026
Merged

[TRTLLM-10947][perf] eagle3: use cudaMemcpy2DAsync custom op for hidden-state capture#14479
pcicotti merged 2 commits into
NVIDIA:mainfrom
pcicotti:llama_eagle3_memcopy

Conversation

@pcicotti

@pcicotti pcicotti commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the Python slice + Tensor.copy_(non_blocking=True) path in Eagle3OneModelSpecMetadata.maybe_capture_hidden_states with a new trtllm::inplace_slice_copy custom op backed by cudaMemcpy2DAsync.

The op:

  • Writes src[:numTokens, :] into dest[:numTokens, dim1_start:dim1_end] in one device-to-device 2-D memcpy on the current PyTorch CUDA stream.
  • Is registered with a Python register_fake (no-op) so it composes with torch.compile / FX tracing.
  • Is added to the piecewise CUDA graph inplace_map (dest is the mutated argument) so the capture continues to inline the write rather than treating it as an external mutation.

Motivation

On the Eagle3 capture path the previous code dispatched a Python aten::slice + aten::copy_ per captured layer per step. Even with non_blocking=True these show up as host-side overhead on the compute timeline and prevent the copy from being fully absorbed into the surrounding CUDA-graph region. Lowering the write to a single cudaMemcpy2DAsync issued on the current stream removes those launches from the timeline.

Performance

Measured on H100 with the EAGLE3 hidden-state-capture repro used during development:

Metric Before After
TTFT 26.6938 ms 25.4725 ms
TPOT 10.2400 ms 7.92125 ms

nsys timeline

image image

Test plan

  • New unit test tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py covers:
    • full-width copy across dtypes (bf16/fp16/fp32)
    • partial-row copy (trailing rows untouched)
    • mid-column slice (flanking columns untouched)
    • layered EAGLE3 capture pattern (multiple layer bands into one dest)
    • empty-src no-op
    • dtype-mismatch error path
    • out-of-bounds slice error path
  • EAGLE3 accuracy unchanged on the repro workload.
  • CI: triggered via /bot run once this leaves draft.

Files

  • cpp/tensorrt_llm/thop/inplaceSliceCopyOp.cpp — new op (cudaMemcpy2DAsync)
  • cpp/tensorrt_llm/thop/CMakeLists.txt — wire op into th_common
  • tensorrt_llm/_torch/custom_ops/__init__.py — Python wrapper + export
  • tensorrt_llm/_torch/custom_ops/cpp_custom_ops.pyregister_fake
  • tensorrt_llm/_torch/compilation/utils.py — add to inplace_info() map
  • tensorrt_llm/_torch/speculative/eagle3.py — call the op in maybe_capture_hidden_states
  • tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py — new unit tests

Summary by CodeRabbit

  • New Features

    • Added an in-place slice copy operation for efficient 2D tensor operations on GPU with support for arbitrary column slicing.
    • Integrated the new operation into speculative decoding for improved performance.
  • Tests

    • Added comprehensive test coverage validating correctness, edge cases, and error handling.

Review Change Stack

…en-state capture

Add trtllm::inplace_slice_copy custom op backed by cudaMemcpy2DAsync, and
use it in Eagle3OneModelSpecMetadata.maybe_capture_hidden_states to replace
the Python slice + Tensor.copy_(non_blocking=True) path. The op is
registered with a Python fake and added to the piecewise CUDA graph
inplace_map so it remains capturable.

Signed-off-by: Pietro Cicotti <5833013+pcicotti@users.noreply.github.com>
@pcicotti
pcicotti marked this pull request as ready for review May 23, 2026 11:27
@pcicotti
pcicotti requested review from a team as code owners May 23, 2026 11:27
@pcicotti
pcicotti requested review from cascade812 and hyukn May 23, 2026 11:27
@pcicotti

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces inplace_slice_copy, a new CUDA-backed Torch custom operator for efficient in-place 2D tensor column-slice copying. The feature includes CUDA kernel implementation with validation, Python API wrappers and fake-op registration, integration into Eagle3's hidden-state capture, and a comprehensive test suite validating correctness and error handling.

Changes

Inplace Slice Copy Custom CUDA Op

Layer / File(s) Summary
CUDA Op Implementation and Build Integration
cpp/tensorrt_llm/thop/inplaceSliceCopyOp.cpp, cpp/tensorrt_llm/thop/CMakeLists.txt
New CUDA op tensorrt_llm::torch_ext::inplaceSliceCopy validates tensor properties (2-D, contiguous, matching dtype, in-bounds slices) and copies source into a column-slice of destination using cudaMemcpy2DAsync. Op is registered via TORCH_LIBRARY_FRAGMENT schema and TORCH_LIBRARY_IMPL CUDA binding; CMakeLists.txt adds the source to the th_common library.
Python API and Compilation Integration
tensorrt_llm/_torch/custom_ops/__init__.py, tensorrt_llm/_torch/custom_ops/cpp_custom_ops.py, tensorrt_llm/_torch/compilation/utils.py
Torch import and inplace_slice_copy() wrapper function forward calls to the CUDA op. Fake-op registration enables shape inference. In-place tracking in inplace_info() marks argument index 1 as the destination tensor for compilation analysis.
Consumer Integration in Eagle3 Speculative Decoding
tensorrt_llm/_torch/speculative/eagle3.py
Eagle3OneModelSpecMetadata.maybe_capture_hidden_states switches from explicit tensor slice + copy_ to calling inplace_slice_copy, delegating the in-place hidden-state write to the custom op.
Comprehensive Functional Test Coverage
tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py
Test suite validates full-width and partial-row copies, middle column-slice writes, layered capture patterns, empty-source no-ops, dtype mismatch errors, and out-of-bounds parameter errors using reference implementations and direct op invocation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% 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
Title check ✅ Passed The title correctly summarizes the main change: introducing a cudaMemcpy2DAsync custom op for Eagle3's hidden-state capture, with clear performance motivation indicated by [perf] tag.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, motivation, performance metrics, test coverage, and file list. All template sections are addressed.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py (1)

1-117: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Add/extend CI perf regression coverage for the Eagle3 path

  • tests/integration/test_lists/qa/llm_perf_sanity.yml already has an Eagle3 entry (perf/test_perf.py::test_perf[qwen3_4b_eagle3-...]), so QA sanity runs have coverage.
  • tests/integration/test_lists/test-db/l0_perf.yml contains no eagle/eagle3 entries, meaning CI perf regression won’t track this optimization—add/update a matching Eagle3 scenario there (using the PR’s model/settings if different from qwen3_4b_eagle3).
🤖 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/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py`
around lines 1 - 117, Add an Eagle3 perf scenario to the CI perf-regression list
by updating the l0_perf.yml test list to include the same Eagle3 entry used by
QA (the perf/test_perf.py::test_perf[qwen3_4b_eagle3-...] style scenario); if
the PR introduces different model/settings, use those exact model ID and
settings for the new entry so CI will track regressions for the Eagle3 path.
Ensure the new entry matches the format of other entries in l0_perf.yml and
passes existing job filters so it runs in the perf regression job.
🧹 Nitpick comments (2)
tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py (2)

104-116: ⚡ Quick win

Consider adding error test coverage for dimension mismatch.

The current error tests validate dtype mismatch and out-of-bounds slice ranges. Consider adding a test for the case where src.shape[1] != (dim1_end - dim1_start), which is another likely user error.

Suggested additional test case
def test_dimension_mismatch_raises():
    """src width must match slice width."""
    dtype = torch.bfloat16
    dest = torch.zeros(8, 32, dtype=dtype, device="cuda")
    src = torch.randn(8, 16, dtype=dtype, device="cuda")  # 16 != (24 - 8)
    with pytest.raises(RuntimeError):
        torch.ops.trtllm.inplace_slice_copy(dest, src, 8, 24)
🤖 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/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py`
around lines 104 - 116, Add a unit test that asserts a RuntimeError is raised
when the source width does not match the requested slice width: create a new
test function (e.g., test_dimension_mismatch_raises) that constructs dest and
src tensors with matching batch dim but src.shape[1] != (dim1_end - dim1_start)
and calls torch.ops.trtllm.inplace_slice_copy(dest, src, dim1_start, dim1_end)
inside pytest.raises(RuntimeError); this mirrors the existing test patterns
(test_dtype_mismatch_raises, test_out_of_bounds_raises) and verifies the
inplace_slice_copy validation for dimension mismatch.

1-117: QA test list updates are not required for this unit test.

This is a focused unit test under tests/unittest/ validating a low-level custom CUDA operator. It does not require entries in the QA integration test lists (tests/integration/test_lists/qa/), as those are intended for end-to-end functional or multi-GPU integration scenarios.

As per coding guidelines: unit-level tests do not need QA list updates unless they represent new end-to-end integration test scenarios.

🤖 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/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py`
around lines 1 - 117, This unit test file tests a low-level CUDA op
(tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py) and
does not require QA integration-list updates, so remove any added entries under
tests/integration/test_lists/qa/ (if you added them) and do not modify those QA
lists; ensure the PR description explicitly states these changes are unit-only
(reference tests like test_full_dest_full_width, test_partial_rows,
test_layered_capture_pattern) so reviewers know no QA-list changes are
necessary.
🤖 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 `@cpp/tensorrt_llm/thop/inplaceSliceCopyOp.cpp`:
- Around line 34-47: dim1Start and dim1End must be validated as non-negative and
device-safe before any pointer arithmetic and kernel launch: add a
TORCH_CHECK(dim1Start >= 0, "dim1Start must be non-negative") and ensure dim1End
> dim1Start (already checked) and dim1End <= dest.size(1) remains; also add a
device equality check (e.g., TORCH_CHECK(dest.device() == src.device(), "dest
and src must be on the same device")) before obtaining raw pointers or launching
CUDA work so destPtr/srcPtr aren't derived from tensors on different devices;
apply the same non-negative index and device checks in the other block
referenced (lines ~59-64) and perform pointer acquisition (data_ptr) only after
these checks.

In `@tensorrt_llm/_torch/speculative/eagle3.py`:
- Around line 443-446: The custom op call should only copy rows for active
tokens; before calling inplace_slice_copy in eagle3.py, truncate to_save to at
most self.num_tokens (e.g., use to_save = to_save[:self.num_tokens]) and pass
that smaller row count into inplace_slice_copy so you don't overwrite inactive
rows of self.hidden_states; update references around inplace_slice_copy,
hidden_states, to_save and ensure this honors any changes made in prepare().

---

Outside diff comments:
In `@tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py`:
- Around line 1-117: Add an Eagle3 perf scenario to the CI perf-regression list
by updating the l0_perf.yml test list to include the same Eagle3 entry used by
QA (the perf/test_perf.py::test_perf[qwen3_4b_eagle3-...] style scenario); if
the PR introduces different model/settings, use those exact model ID and
settings for the new entry so CI will track regressions for the Eagle3 path.
Ensure the new entry matches the format of other entries in l0_perf.yml and
passes existing job filters so it runs in the perf regression job.

---

Nitpick comments:
In `@tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py`:
- Around line 104-116: Add a unit test that asserts a RuntimeError is raised
when the source width does not match the requested slice width: create a new
test function (e.g., test_dimension_mismatch_raises) that constructs dest and
src tensors with matching batch dim but src.shape[1] != (dim1_end - dim1_start)
and calls torch.ops.trtllm.inplace_slice_copy(dest, src, dim1_start, dim1_end)
inside pytest.raises(RuntimeError); this mirrors the existing test patterns
(test_dtype_mismatch_raises, test_out_of_bounds_raises) and verifies the
inplace_slice_copy validation for dimension mismatch.
- Around line 1-117: This unit test file tests a low-level CUDA op
(tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py) and
does not require QA integration-list updates, so remove any added entries under
tests/integration/test_lists/qa/ (if you added them) and do not modify those QA
lists; ensure the PR description explicitly states these changes are unit-only
(reference tests like test_full_dest_full_width, test_partial_rows,
test_layered_capture_pattern) so reviewers know no QA-list changes are
necessary.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b14b389a-cb90-456c-8f8d-7138000a3e58

📥 Commits

Reviewing files that changed from the base of the PR and between ad7c206 and 4810e36.

📒 Files selected for processing (7)
  • cpp/tensorrt_llm/thop/CMakeLists.txt
  • cpp/tensorrt_llm/thop/inplaceSliceCopyOp.cpp
  • tensorrt_llm/_torch/compilation/utils.py
  • tensorrt_llm/_torch/custom_ops/__init__.py
  • tensorrt_llm/_torch/custom_ops/cpp_custom_ops.py
  • tensorrt_llm/_torch/speculative/eagle3.py
  • tests/unittest/_torch/thop/parallel_hw_agnostic/test_inplace_slice_copy.py

Comment thread cpp/tensorrt_llm/thop/inplaceSliceCopyOp.cpp
Comment thread tensorrt_llm/_torch/speculative/eagle3.py
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #50046 [ run ] triggered by Bot. Commit: 4810e36 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #50046 [ run ] completed with state SUCCESS. Commit: 4810e36
/LLM/main/L0_MergeRequest_PR pipeline #39607 completed with status: 'SUCCESS'

CI Report

Link to invocation

Address review feedback on inplace_slice_copy:
- TORCH_CHECK that dest and src are on the same CUDA device before
  obtaining raw pointers and launching the copy on dest's stream.
- TORCH_CHECK that dim1Start is non-negative so the destPtr offset
  cannot underflow before the dest allocation.

Add unit tests test_negative_dim1_start_raises and
test_device_mismatch_raises covering the new error paths.

Signed-off-by: Pietro Cicotti <5833013+pcicotti@users.noreply.github.com>
@pcicotti

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #50106 [ run ] triggered by Bot. Commit: b121850 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #50106 [ run ] completed with state SUCCESS. Commit: b121850
/LLM/main/L0_MergeRequest_PR pipeline #39659 completed with status: 'SUCCESS'

CI Report

Link to invocation

@pcicotti
pcicotti enabled auto-merge (squash) June 1, 2026 13:47
@pcicotti
pcicotti merged commit 06456e1 into NVIDIA:main Jun 1, 2026
8 checks passed
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.

5 participants