Skip to content

Let the CUDA allocator manage cuBLAS workspaces - #3625

Open
Vtmpas wants to merge 23 commits into
NVIDIA:mainfrom
Vtmpas:fix/cublas-workspace-per-stream
Open

Vtmpas wants to merge 23 commits into
NVIDIA:mainfrom
Vtmpas:fix/cublas-workspace-per-stream

Conversation

@Vtmpas

@Vtmpas Vtmpas commented Oct 5, 2026 •

Copy link
Copy Markdown

Description

Concurrent GEMMs on independent CUDA streams receive the same cuBLAS workspace from the device/mode cache and can overwrite each other's partial sums. On one H200, two FP32 NT GEMMs with a reduction dimension of 16384 produced incorrect results in 35 of 48 calls; separate scratch produced zero mismatches against serial execution.

Replace the Python workspace cache with invocation-local memory owned by PyTorch's CUDA allocator. Ordinary GEMMs allocate a native DataPtr in the PyTorch binding, avoiding per-call Python Tensor creation. The list/multistream grouped path uses one allocation with disjoint, 256-byte-aligned workspace slices. Its helper streams join the caller before the owner is released. Eager reuse remains stream-ordered; captured allocations belong to their graph's private pool. No persistent per-stream cache or new device synchronization is added.

Custom recipes and communication overlap retain explicit Tensor workspaces. Existing explicit-workspace callers remain supported; the private bindings additionally accept None with size zero to request native allocation. Usable workspace sizes, GEMM dtypes and algorithm selection are unchanged. The grouped-tensor backend is unchanged.

Type of change

  • Documentation change
  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change
  • Infra/Build change
  • Code refactoring

Changes

  • Allocate scratch per invocation in the PyTorch native binding for ordinary and list/multistream grouped GEMMs.
  • Remove Linear and BasicLinear's preallocation workaround for globally cached scratch.
  • Test eager ownership, temporary-stream cleanup, graph-pool isolation across capture generations, concurrent FP32/BF16 graph replay, grouped GEMM numerics, and compiled opaque-GEMM graph generations. Observe native allocation requests without retaining Tensor references.
  • Cover router dgrad NN with weight[128,4096], grad_output[8192,128], output [8192,4096], and reduction 128 against exact analytic references in FP32 and BF16. Preserve NT coverage.
  • Include the workspace regression in the PyTorch L0 launcher.

Validation

H200, PyTorch 2.13.0+cu130, CUDA toolkit 13.0, cuBLASLt 13.5.1, native PyTorch CUDA allocator. The candidate binding was rebuilt with the same flags as main 9ea05cf2e8dc, using the unchanged common library.

  • All 22 cases in tests/pytorch/test_gemm_workspace.py pass, including the compiled opaque-GEMM capture-generation case, with no skips.
  • Three additional local operational regressions pass. The same independent-stream and concurrent graph-generation fixtures fail on main; both the prior PR revision and native implementation pass. Temporary-stream cleanup and invalid-request checks also pass.
  • A two-H200 quantized-wrapper check confirms that device selection follows actual storage and restores the caller's device.
  • All 288 paired benchmark comparisons have bitwise-identical outputs and module input/parameter gradients; 18 targeted recheck comparisons also match.
  • Six alternating main/candidate process pairs cover three shapes, BF16/block FP8, GEMM/grouped GEMM/Linear/GroupedLinear, and eager/graph execution. Small eager GEMM completion overhead is +1.65% BF16 and +3.48% FP8; grouped GEMM is +0.14%/+0.59%. Small Linear forward/backward is +0.05%/+1.51%, and GroupedLinear is -0.53%/-0.11%. These are paired medians, not universal bounds. CUDA graph replay is measured separately; median changes are within ±1% in these cases. Detailed timing, warmup flags and rechecks are in the benchmark comments below.
  • Pre-commit, scoped pylint/cpplint and the L0 license checker pass on macOS. Changes after GPU qualification are repository formatting only.
  • Follow-up in 48a48c91896a: update both Linear compile-test observers to the native binding. Both edited observers pass H200 capture checks through the compiled-GEMM fixture; the full Linear tests remain capability-skipped (11 cases) on the available PyTorch 2.13 build, pending the compatible opaque-object CI environment.
  • Full L0 validation of the new native revision remains pending CI. These tests do not establish full-model throughput, distributed-overlap performance or alternate allocator support.

Checklist

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

The workspace cache currently ignores the calling CUDA stream. Overlapping
GEMMs can therefore overwrite the same scratch buffer and return incorrect
results.

Include the stream in the cache key, keep reuse on each stream, and add
ownership and FP32/BF16 numerical regression tests.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Oct 5, 2026
@Vtmpas
Vtmpas marked this pull request as ready for review October 5, 2026 10:06
@Vtmpas
Vtmpas requested a review from ksivaman as a code owner October 5, 2026 10:06
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new blocking issues were found.

Summary

The PR replaces cached cuBLAS scratch with memory owned by PyTorch’s CUDA allocator for each call.

  • Ordinary GEMMs get scratch from PyTorch’s allocator for each call.
  • List-based grouped GEMMs get aligned scratch for their helper streams.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[GEMM call] --> B{Execution path}
  B -->|Ordinary native| C[Allocate scratch in C++]
  B -->|List-based grouped| D[Allocate separate aligned slices]
  B -->|Custom recipe or communication overlap| E[Allocate explicit Tensor workspace]
  C --> F[Submit GEMM on caller stream]
  D --> G[Run grouped GEMMs]
  G --> H[Join helper streams to caller]
  E --> I[Run existing explicit-workspace path]
  F --> J[Release local owner]
  H --> J
  I --> J
  J --> K[PyTorch controls reuse and graph-pool lifetime]
Loading

Reviews (22) · Last reviewed commit: "Merge branch 'main' into fix/cublas-work..." · Reviewed by Greptile

Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py Outdated
Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py Outdated
@Vtmpas
Vtmpas marked this pull request as draft October 5, 2026 10:30
Keep scratch local to each invocation so graph captures use their own
private pools and temporary streams do not retain tensors in Python.
Remove Linear's preallocation workaround for the global workspace cache.

Cover concurrent graph replays, graph generations, scratch reclamation,
grouped GEMM stream joins, and compiled Linear forward and backward.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@Vtmpas Vtmpas changed the title Cache cuBLAS workspace by CUDA stream Let the CUDA allocator manage cuBLAS workspaces Oct 5, 2026
@Vtmpas
Vtmpas marked this pull request as ready for review October 5, 2026 10:44
Vtmpas and others added 3 commits October 5, 2026 14:08
Cover NN router dgrad with FP32 and BF16 operands in concurrent eager execution and independent CUDA graphs. Preserve NT wgrad coverage and exact references while retaining only one pair of large eager outputs.

All 22 workspace cases pass on native SM90 H200 with CUDA 13 and PyTorch 2.13, including all four new NN cases.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@Vtmpas

Vtmpas commented Oct 6, 2026

Copy link
Copy Markdown
Author

@Oleg-Goncharov , @ksivaman , hello! can i have your assistance here?

@Vtmpas

Vtmpas commented Oct 8, 2026

Copy link
Copy Markdown
Author

@ksivaman kindly

@ksivaman

ksivaman commented Oct 8, 2026

Copy link
Copy Markdown
Member

/te-ci pytorch L0

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

had the same trouble, thanks

@greptile-apps

This comment has been minimized.

@greptile-apps

greptile-apps Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

Replace the cold-start test's cache_info assertions with checks for capture-time allocation and release of workspace tensors. Keep output and gradient comparisons across CUDA, CPU and meta initialization.

Remove BasicLinear's obsolete cache preallocation and use the existing opaque-object capability guard for compiled execution.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@Vtmpas
Vtmpas requested a review from timmoon10 as a code owner October 8, 2026 21:45
@Vtmpas

Vtmpas commented Oct 8, 2026

Copy link
Copy Markdown
Author

Fixed in f01b35e, on top of the latest main merge. The cold-start test now checks capture-time workspace allocation and release of Python tensor references instead of cache_info(). BasicLinear no longer preallocates scratch for the removed cache. The six initialization/training combinations, fullgraph capture checks, and output/gradient comparisons are preserved.

On H200 with CUDA 13 and PyTorch 2.13, the original AttributeError reproduced before the fix. Afterward, 22 workspace tests, the fake-initialization test, and three BasicLinear CUDA-graph cases passed. The six compiled cold-start cases are capability-skipped because this PyTorch lacks register_custom_class; execution of those cases on a compatible PyTorch build remains unverified. Formatting, scoped pylint, and the license check passed.

@Vtmpas

Vtmpas commented Oct 9, 2026

Copy link
Copy Markdown
Author

@timmoon10 @ksivaman conflicts have been solved
ci seems okay

@ksivaman

ksivaman commented Oct 9, 2026

Copy link
Copy Markdown
Member

/te-ci pytorch L0

@Vtmpas

Vtmpas commented Oct 10, 2026

Copy link
Copy Markdown
Author

@ksivaman thanks for kicking off CI again. The last run stopped at pytorch-py3-base-amd64 before any tests ran, and the public log is empty, so I can't tell what went wrong. The previous run passed everything except the H100 unittest, which was the cache_info() test fixed in f01b35e. Could you rerun it, or let me know if it's something on my side?

@Vtmpas

Vtmpas commented Oct 12, 2026

Copy link
Copy Markdown
Author

@timmoon10 @ksivaman @ptrendx , sorry guys, are there any updated or feedback? Could you guide me here what's blocking, please?

@Vtmpas

Vtmpas commented Oct 15, 2026

Copy link
Copy Markdown
Author

Kindly

@Vtmpas

Vtmpas commented Oct 19, 2026

Copy link
Copy Markdown
Author

@ksivaman are there any updates?

ksivaman and others added 2 commits October 20, 2026 09:36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com>
Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com>
@ksivaman

Copy link
Copy Markdown
Member

/te-ci pytorch L0 L1

@ksivaman

Copy link
Copy Markdown
Member

For the future we should add the same change to the general grouped GEMM to mitigate the risk for that as well.

@Vtmpas

Vtmpas commented Oct 20, 2026 •

Copy link
Copy Markdown
Author

For the future we should add the same change to the general grouped GEMM to mitigate the risk for that as well.

I can open a separate PR for this if you’re okay with it.

@ksivaman

Copy link
Copy Markdown
Member

I am worried about re-adding the very high CPU overhead associated with allocated workspaces per gemm. Some of the work in #2377 was done to mitigate this. Although this is higher priority due to correctness, we should make a note of the added overhead via some benchmarking.

@Vtmpas

Vtmpas commented Oct 22, 2026

Copy link
Copy Markdown
Author

I am worried about re-adding the very high CPU overhead associated with allocated workspaces per gemm. Some of the work in #2377 was done to mitigate this. Although this is higher priority due to correctness, we should make a note of the added overhead via some benchmarking.

Good point. Reusing storage through the CUDA allocator still leaves tensor creation and allocator overhead on every eager call. An earlier H200 microbenchmark measured about 1.31 µs for the intermediate per-stream cache versus 2.62 µs for invocation-local allocation. That only measured the workspace factory, though, and neither side represents a current main-vs-PR comparison. The module timings I collected were also too noisy to establish a reliable regression bound.
I’ll add a warmed-up comparison against main for the current revision, covering CPU overhead and end-to-end Linear/GroupedLinear forward-backward latency, especially small GEMMs where the overhead is more exposed. I’ll report CUDA graph replay separately and document the measured cost in the PR.

@Vtmpas

Vtmpas commented Oct 22, 2026

Copy link
Copy Markdown
Author

@ksivaman I ran the current PR against main on H200. The CPU cost is real, particularly for small eager GEMMs.

Compared main 9ea05cf2e8dc with PR 769e7ed11e48. This includes the cached workspace-size lookup added during review. Both used their complete Python trees and the same freshly built native libraries: there are no native source changes between these revisions.

The warmed workspace factory, including releasing the returned reference, measured:

CPU time per call main PR Added time, paired median [IQR]
Single workspace 0.058 µs 1.892 µs +1.834 µs [1.812, 1.863]
Grouped workspace list (4 buffers) 0.057 µs 6.873 µs +6.816 µs [6.791, 6.967]

These are thread CPU times with no GEMM enqueued, so they isolate the factory cost from GPU execution. The older 1.31/2.62 µs figures in my previous reply are not the baseline for this comparison.

For actual eager execution, the small (M,K,N)=(128,256,256) BF16 GEMM went from 13.59 to 16.53 µs (+21.8%). Linear forward+backward at that shape went from 163.61 to 178.25 µs (+8.2%), and GroupedLinear with four groups from 425.71 to 465.25 µs (+9.6%). A single 4096³ GEMM was 175.04 → 175.22 µs (+0.2%) in BF16 and 110.01 → 110.20 µs (+0.2%) in block FP8. The extra host work is exposed in the small cases and largely hidden in those large single-GEMM cases.

CUDA graph replay is reported separately below; capture is excluded. It does not execute the Python workspace factory on every replay. I would not claim a blanket zero-overhead result: large BF16 Linear and large grouped block-FP8 GEMM had substantial pair-to-pair variation, including changes of both signs.

Full latency results: BF16 and block FP8, eager and graph replay

All latencies are µs per GEMM call or per module forward+backward. Values are medians over six independent process pairs. Changes are the median of paired PR/main ratios, with their interquartile range in brackets; they are not calculated from the two separately aggregated latency medians. Positive means slower. IQR describes the observations, not a confidence interval.

Grouped cases have four groups, with M rows per group. Shapes: small 128×256×256, skinny 128×4096×128, large 4096×4096×4096, in M×K×N order. A dagger marks a case with a warmup-variance flag in at least one initial process; its separate longer-warmup recheck follows.

BF16

Shape Operation Eager main → PR Change [IQR] Graph main → PR Change [IQR]
small GEMM 13.59 → 16.53 +21.8% [+21.4, +22.3] 3.67 → 3.71 † +1.3% [-2.6, +3.4]
small Grouped GEMM 48.76 → 58.10 † +19.1% [+18.7, +19.6] 8.67 → 8.67 † -0.3% [-1.9, +1.5]
small Linear fwd+bwd 163.61 → 178.25 +8.2% [+6.4, +10.6] 112.70 → 114.09 +0.8% [-0.4, +2.1]
small GroupedLinear fwd+bwd 425.71 → 465.25 +9.6% [+8.4, +9.8] 159.73 → 161.59 +1.2% [-1.2, +1.5]
skinny GEMM 14.84 → 17.69 +19.4% [+18.9, +19.8] 7.09 → 7.06 -0.4% [-1.8, +0.6]
skinny Grouped GEMM 53.43 → 62.98 +17.8% [+17.4, +18.0] 10.86 → 10.99 +1.2% [-2.0, +3.6]
skinny Linear fwd+bwd 170.03 → 186.09 +9.3% [+6.8, +10.1] 113.42 → 114.56 † +0.2% [-1.5, +1.0]
skinny GroupedLinear fwd+bwd 439.47 → 479.15 +8.5% [+7.5, +9.4] 163.92 → 164.66 +1.0% [-1.2, +1.5]
large GEMM 175.04 → 175.22 +0.2% [+0.1, +0.4] 174.62 → 174.69 +0.1% [-0.1, +0.1]
large Grouped GEMM 831.02 → 833.65 +1.2% [+0.0, +3.4] 806.17 → 815.21 +1.0% [+0.1, +2.6]
large Linear fwd+bwd 578.74 → 599.35 +3.8% [-0.4, +8.9] 627.01 → 617.91 -2.0% [-7.6, +4.8]
large GroupedLinear fwd+bwd 2515.99 → 2514.83 † -0.1% [-0.1, +0.5] 2604.25 → 2606.32 +0.0% [-0.1, +0.1]

FP8 block scaling

Shape Operation Eager main → PR Change [IQR] Graph main → PR Change [IQR]
small GEMM 15.79 → 18.74 +18.4% [+18.0, +19.3] 4.71 → 4.79 +2.1% [-1.6, +3.2]
small Grouped GEMM 55.27 → 65.03 +17.4% [+16.9, +17.8] 9.41 → 9.64 +2.5% [-1.7, +6.1]
small Linear fwd+bwd 276.71 → 299.48 +7.8% [+7.4, +8.6] 110.94 → 111.74 +0.3% [-1.0, +1.0]
small GroupedLinear fwd+bwd 680.38 → 725.05 +6.4% [+5.5, +7.4] 155.86 → 157.27 +0.4% [-0.2, +1.2]
skinny GEMM 16.01 → 19.08 +19.1% [+18.2, +19.8] 14.77 → 14.77 -0.0% [-0.3, +0.3]
skinny Grouped GEMM 55.99 → 65.71 +17.0% [+16.6, +17.2] 17.94 → 18.04 +0.2% [-0.1, +0.5]
skinny Linear fwd+bwd 282.73 → 299.72 † +6.2% [+5.2, +7.2] 111.46 → 113.07 +1.0% [+0.0, +2.0]
skinny GroupedLinear fwd+bwd 691.70 → 738.58 +6.0% [+5.6, +6.8] 157.86 → 160.03 +1.2% [-0.2, +2.2]
large GEMM 110.01 → 110.20 +0.2% [+0.1, +0.2] 109.45 → 109.56 +0.1% [+0.1, +0.1]
large Grouped GEMM 480.53 → 486.00 -2.1% [-5.0, +8.0] 484.61 → 494.82 +2.4% [+0.6, +3.6]
large Linear fwd+bwd 446.39 → 472.33 +4.6% [+1.6, +6.0] 504.67 → 504.20 +0.9% [-0.5, +1.9]
large GroupedLinear fwd+bwd 1936.71 → 1935.33 -0.1% [-0.6, +0.2] 2034.87 → 2035.99 +0.1% [-0.0, +0.2]
Targeted recheck of the six warmup-flagged cases

The initial warmup screen flagged a >15% range over its last three batches. I retained those measurements and repeated only the six affected configurations in another six alternating process pairs, with 10×100 warmup calls and 7×100 timed calls per process. These are a separate series; the table above has not been overwritten.

Case main → PR (µs) Change [IQR]
gemm/tiny/bf16/graph 3.33 → 3.21 -2.3% [-3.5, -0.4]
grouped_gemm/tiny/bf16/eager 45.60 → 54.71 +20.2% [+19.5, +20.8]
grouped_gemm/tiny/bf16/graph 8.38 → 8.19 -1.4% [-2.1, -0.7]
grouped_linear/large/bf16/eager 2563.20 → 2565.87 +0.1% [+0.1, +0.1]
linear/skinny/bf16/graph 112.00 → 111.39 -0.4% [-0.9, -0.1]
linear/skinny/fp8_block/eager 279.16 → 292.60 +4.7% [+3.8, +5.1]

No warmup-variance flags remained in this recheck, and outputs/gradients matched bitwise in all 36 additional paired case comparisons.

Environment, timing and correctness checks
  • One H200, Xeon Platinum 8570, one pinned host thread, default GPU clocks; PyTorch 2.13.0+cu130, CUDA toolkit 13.0, cuBLASLt runtime 13.5.1, driver 615.71.09, native PyTorch caching allocator.
  • Six main/PR process pairs, alternating AB/BA order. Each configuration used 50 timed warmup calls plus setup warmup, then seven batches of 20 calls. Factory measurements used seven batches of 20,000 calls after 1,000 warmup calls. Cyclic GC was disabled during timing.
  • End-to-end wall time includes completion, synchronized at batch boundaries rather than after each call. It is not a pure GPU-kernel measurement. Host enqueue time can also include runtime backpressure. CUDA-event intervals were recorded separately and not relabelled as kernel-only time.
  • Raw GEMMs use TN layout and preallocated BF16 output. Raw FP8 inputs are block-quantized before timing; module FP8 includes quantization. Modules use BF16 parameters, no bias, no optimizer or FP8 weight cache, and reset gradients each independent step. GroupedLinear explicitly uses use_grouped_tensor=False, the affected list/multistream path. Module graphs use TE's forward/backward graph wrappers; raw GEMM graphs replay a single captured operation. This is not a full training-step or distributed benchmark. PyTorch emitted an AccumulateGrad stream-mismatch warning in both module-graph setups; capture/replay completed, but these wrapper-inclusive timings should not be read as pure graph execution time.
  • All 48 operation configurations had bitwise-identical outputs between main and PR in all six pairs; input/parameter gradients also matched bitwise for all module cases. Repeated and post-timing values were also checked. This is numerical coverage for these fixtures, not a substitute for the concurrent-stream/graph-ownership regression tests.
  • A separate Nsight Systems capture of a small BF16 GEMM (128×512×512, 100 calls after warmup) showed the same kernel, nvjet_sm90_tst_64x8_64x16_1x2_h_bz_TNT, and no cudaMalloc/cudaFree calls in either warmed capture. Profiling was off for all reported latency measurements. The allocator reuses storage, but tensor/allocator work on the host still costs time.

Use an invocation-local DataPtr from the PyTorch CUDA allocator instead
of creating Python workspace tensors for ordinary GEMMs. Allocate one
buffer with aligned, disjoint slices for the grouped multistream path.

Keep stream-ordered reuse and graph-pool ownership without adding a
persistent cache. Preserve explicit workspaces for custom recipes and
communication overlap, and observe native requests in the compiled
graph-generation regression.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@Vtmpas

Vtmpas commented Oct 22, 2026

Copy link
Copy Markdown
Author

@ksivaman I pushed 10f6369 to address the host overhead without bringing back a workspace cache.

Ordinary GEMMs now get an invocation-local DataPtr directly from PyTorch's CUDA allocator in the C++ binding. This removes Python Tensor creation from that path. The list/multistream grouped path uses one allocation with disjoint, 256-byte-aligned slices; its helper streams join the caller before the owner is released. Allocation lifetime still follows the caller stream and capture pool. UB overlap and custom recipes keep their explicit Tensor workspaces; the grouped-tensor backend is unchanged.

I reran the comparison against main 9ea05cf2e8dc on H200, rebuilding the candidate binding with the same compiler flags and unchanged common library. These are new paired measurements, not a comparison between absolute timings from different runs.

Eager completion latency vs main BF16 Block FP8
Small GEMM +1.65% [+0.38, +2.11] +3.48% [+2.63, +3.65]
Small grouped GEMM (4 groups) +0.14% [-0.28, +0.92] +0.59% [+0.02, +1.14]
Small Linear forward + backward +0.05% [-2.35, +1.48] +1.51% [-0.43, +2.49]
Small GroupedLinear forward + backward -0.53% [-1.97, +0.37] -0.11% [-1.28, +0.14]
4096³ GEMM -0.25% [-0.78, +0.61] +0.21% [-0.01, +0.40]

Small is (M,K,N)=(128,256,256), with M rows per group. Values are median paired changes across six alternating AB/BA process pairs, with IQR in brackets; positive means slower. The remaining small single-GEMM cost is measurable, but the module and grouped results are much closer to main than the Python-allocation revision.

For host enqueue time, the small BF16 GEMM adds 0.211 µs, the small block-FP8 GEMM 0.518 µs, and the small BF16 grouped call 0.056 µs (paired medians). These measure the whole submission path and can include runtime backpressure; they are not isolated allocator CPU timings. The old Python factory microbenchmark no longer represents the ordinary GEMM path.

Correctness:

  • All 22 workspace tests pass, including the compiled opaque-GEMM graph-generation case, with no skips. Its observer now checks native workspace requests instead of the removed Python allocation call.
  • Three additional local regressions pass: independent-stream NT/NN analytic numerics, concurrent replay of separate graph generations, and invalid-request/temporary-stream cleanup. The same fixtures fail the first two tests on main and pass on the previous PR revision as well as this implementation.
  • A separate two-H200 check passes for a quantized wrapper whose Tensor metadata device differs from its underlying storage, including restoration of the caller's device.
  • Outputs and module input/parameter gradients match main bitwise in all 288 paired case comparisons, plus 18 comparisons in the targeted rechecks.
Measurement details and limits

One H200 for timing, Xeon Platinum 8570 with the host process pinned to one CPU, default GPU clocks, PyTorch 2.13.0+cu130, CUDA toolkit 13.0, cuBLASLt 13.5.1, driver 615.71.09, native PyTorch caching allocator. Six AB/BA pairs; each case has 10×100 warmup calls and 7×100 timed calls. Shapes were 128×256×256, 128×4096×128 and 4096³, covering raw GEMM, list/multistream grouped GEMM, Linear and GroupedLinear forward/backward in BF16 and block FP8, both eager and graph replay.

Completion time synchronizes at batch boundaries. Raw GEMMs use TN layout and preallocated BF16 output; raw FP8 quantization is outside timing, while module quantization is included. Modules use BF16 parameters, no bias, optimizer or FP8 weight cache, and reset gradients each call. Grouped cases use four groups and explicitly select the list/multistream backend.

CUDA graph capture is excluded from replay timing. Median paired replay changes were within ±1% across the measured cases; this is an observation, not a zero-overhead guarantee. Module graph timings include TE's graph/autograd wrapper. The existing AccumulateGrad stream warning occurred during setup in both variants; capture and replay completed.

Three initial cases had warmup-variance flags: small FP8 eager GEMM, large FP8 eager GEMM and small FP8 Linear graph replay. Those were retained and separately repeated in six pairs with the same warmup/timing counts; no flags remained. The table uses the rechecks for the two affected eager GEMMs. IQR describes the observed pairs, not a confidence interval.

A separate warmed Nsight Systems capture used the same BF16 GEMM kernel and showed no cudaMalloc/cudaFree calls. Profiling was disabled for latency measurements. Final source changes after the GPU runs were Black/clang-format formatting only; pre-commit, scoped pylint/cpplint and the license check pass.

This qualifies the tested native-allocator paths on H200, not full-model throughput, distributed overlap or every allocator backend. Full L0 still needs CI.

Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py
Observe generic_gemm requests during capture instead of the Python
workspace factory, which ordinary GEMMs no longer call. Retain only
scalar metadata and require the native None/zero workspace contract.

Keep the cold-start initialization check by rejecting Python workspace
allocation, and preserve the existing fullgraph, capture, and numerical
assertions for every initialization and training case.

Signed-off-by: Matvey Saprykin <mtvey.s@gmail.com>
@Vtmpas

Vtmpas commented Oct 22, 2026

Copy link
Copy Markdown
Author

@ksivaman could you rerun PyTorch L0 on 48a48c9 and take another look at the fix when you have a chance? Thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants