Skip to content

[JAX] FFI initialize stage captures XLA's compute stream, swallowing work other threads enqueue on it #3863

Open

Description

@uzhilinsky

Describe the bug

wrapInStreamCapture (transformer_engine/jax/csrc/extensions/ffi.h) begins a capture on the stream XLA passes to the FFI "initialize" stage, which is XLA's per-device compute stream. Relaxed capture mode doesn't stop other threads from enqueueing on that stream while the capture is open. Any work another host thread puts on it during a program's first execution (an H2D transfer recording its compute-stream sync event, or a small jitted program run by a data-loading thread) goes into the graph that is then destroyed. The program then fails, crashes, or the other thread's work silently never runs.

Typical error:

XLA FFI call failed: .../util/vectorized_pointwise.h:367 in function VectorizedUnaryKernelLauncher: CUDA Error: operation failed due to a previous error during capture

We have also seen a segfault in cuEventRecord under CopyRawHostToDeviceAndReturnEvent → GetEventForComputeStreamSyncPoint, and hangs.

Steps/Code to reproduce bug

One GPU (B300), TE 2.20.2, jax 0.12. A background thread continuously device_puts batches and runs a small jitted dynamic_update_slice into a resident device buffer. Meanwhile the main thread first-executes a fresh jitted program using TE layernorm → activation → an NVFP4 GEMM (forward and backward), which goes through the NormFwd/Bwd, ActLu and RHTAmax initialize handlers. Each trial runs in its own process:

trials failed
stock TE, background thread placing data 10 10 (9 capture errors, 1 crash)
stock TE, no background thread 10 0
stock TE, "initialize" stage not registered 10 0
patched TE (below), background thread placing data 10 0
patched TE, each capture held open 20 ms longer 6 0 (stock under the same widening: 6/6 failed)

Proposed fix

Capture on a TE-owned stream, not XLA's. Module loading, which the initialize stage exists for (#2219), is per context, not per stream. The five handlers wrapped today only pass the stream to nvte_* launchers (the cuDNN norm path rebinds its handle every call), so nothing depends on which stream they ran on. With the patch, NVFP4 and MXFP8 forward and backward outputs are bitwise identical to stock, on both first and second execution.

+// Captures on a TE-owned stream, never the one XLA passes: work other threads enqueue on XLA's
+// stream while the capture is open would otherwise be swallowed into the discarded graph.
+inline cudaStream_t captureStreamForCurrentDevice() {
+  thread_local std::unordered_map<int, cudaStream_t> streams;
+  int device = 0;
+  NVTE_CHECK_CUDA(cudaGetDevice(&device));
+  auto [it, inserted] = streams.try_emplace(device, nullptr);
+  if (inserted) {
+    NVTE_CHECK_CUDA(cudaStreamCreateWithFlags(&it->second, cudaStreamNonBlocking));
+  }
+  return it->second;
+}
+
 template <typename... Args>
 Error_Type wrapInStreamCapture(std::function<Error_Type(cudaStream_t, Args...)> func,
-                               cudaStream_t stream, Args... args) {
+                               cudaStream_t /*stream*/, Args... args) {
+  cudaStream_t capture = captureStreamForCurrentDevice();
   cudaGraph_t graph{};
-  NVTE_CHECK_CUDA(cudaStreamBeginCapture(stream, cudaStreamCaptureModeRelaxed));
-  Error_Type error = func(stream, std::forward<Args>(args)...);
-  NVTE_CHECK_CUDA(cudaStreamEndCapture(stream, &graph));
+  NVTE_CHECK_CUDA(cudaStreamBeginCapture(capture, cudaStreamCaptureModeRelaxed));
+  Error_Type error = func(capture, std::forward<Args>(args)...);
+  NVTE_CHECK_CUDA(cudaStreamEndCapture(capture, &graph));
   NVTE_CHECK_CUDA(cudaGraphDestroy(graph));

#2800 adds initialize stages to thirteen more FFIs, which would widen this window to most TE programs.

Environment

TE 2.20.2 built from the PyPI sdist against jax[cuda13] 0.12.0.1, CUDA 13, B300, single process.

Activity

  1. uzhilinsky commented on Nov 13, 2026

    @uzhilinsky
    Author

    Fix with a regression test: #3864.

  2. added a commit that references this issue on Nov 13, 2026
    2a9f23d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions