Skip to content

cuda-gen: Add CUDA Graph capture and replay for composite operators - #1987

Open
Nafees01 wants to merge 19 commits into
CEED:mainfrom
Nafees01:cuda-graph-dev
Open

cuda-gen: Add CUDA Graph capture and replay for composite operators#1987
Nafees01 wants to merge 19 commits into
CEED:mainfrom
Nafees01:cuda-graph-dev

Conversation

@Nafees01

@Nafees01 Nafees01 commented Jul 8, 2026

Copy link
Copy Markdown

Summary

Adds CUDA Graph capture and replay to the 'cuda-gen' backend for composite operators. On the second apply, the kernel sequence is captured into a cudaGraph; all subsequent applies replay the instantiated graph. QFunction contexts are re-synced to device before each replay so time- and load-dependent parameters stay correct.

Sub-operators that cannot be captured, for example, contact operators that allocate memory (cudaMalloc) during apply, which CUDA doesn't allow inside a capture. When this happens, the capture is properly closed to leave the stream in a clean state, any leftover CUDA error is cleared, and that operator falls back to running normally on every call.

Validated against both /gpu/cuda/ref and cuda-gen without graphs across static, quasistatic, contact, multi-material, and dynamic elasticity examples. Strain energy and displacements match to ~1e-13 in every case, with zero graph fallbacks on capturable operators.

Performance-wise, graphs are within ±1% of cuda-gen without graphs across all sizes, so no regression, but no real speedup either. The reason is that cuda-gen already fuses everything into a few large kernels, so there's very little launch overhead left for graphs to remove.

Validated correctness against /gpu/cuda/ref and cuda-gen (CEED_DISABLE_GRAPH=1) on the following Ratel examples:

  • ex01-static-elasticity-linear-mms
  • ex02-quasistatic-elasticity-linear-platen (contact)
  • ex02-quasistatic-elasticity-neo-hookean-current-sinker (multi-material)
  • ex03-dynamic-elasticity-mooney-rivlin-current
    Strain energy matches to ~1e-13 in all cases. Zero graph fallbacks on capturable operators.

@zatkins-dev

Copy link
Copy Markdown
Collaborator

Sub-operators that cannot be captured, for example, contact operators that allocate memory (cudaMalloc) during apply

Are there specific operators you were unable to use in Ratel? None of the QFunctions there should alloc during operator application.

@zatkins-dev

Copy link
Copy Markdown
Collaborator

There's some style errors here, be sure to make format each commit

@zatkins-dev zatkins-dev 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.

I'm sure you're still working on this, but here's an initial review to correct some high-level design issues and removed functionality that I think should be fixed.

Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
@zatkins-dev

Copy link
Copy Markdown
Collaborator

I would be interested to see if this speeds up e.g. /gpu/cuda/ref, when recording the graph over the entire CeedOperatorApplyCore_Cuda_Ref function

@jeremylt

Copy link
Copy Markdown
Member

Note - merging main into your branch instead of rebasing means I'd recommend we squash-merge this PR

@Nafees01

Copy link
Copy Markdown
Author

Sub-operators that cannot be captured, for example, contact operators that allocate memory (cudaMalloc) during apply

Are there specific operators you were unable to use in Ratel? None of the QFunctions there should alloc during operator application.

The only example where the contact (Nitsche) sub-operator couldn't be captured was the quasistatic contact case (ex02-quasistatic-elasticity-linear-platen). Everything else I tried static, dynamic, multi-material, neo-Hookean, Mooney-Rivlin captured and replayed fine.

And you're right that the QFunction doesn't allocate; the cudaMalloc comes from libCEED's own device-side handling of the contact data, not the QFunction itself. It runs either way correctly; the contact operators just fall back to a normal apply rather than being replayed.

@zatkins-dev

Copy link
Copy Markdown
Collaborator

Sub-operators that cannot be captured, for example, contact operators that allocate memory (cudaMalloc) during apply

Are there specific operators you were unable to use in Ratel? None of the QFunctions there should alloc during operator application.

The only example where the contact (Nitsche) sub-operator couldn't be captured was the quasistatic contact case (ex02-quasistatic-elasticity-linear-platen). Everything else I tried static, dynamic, multi-material, neo-Hookean, Mooney-Rivlin captured and replayed fine.

And you're right that the QFunction doesn't allocate; the cudaMalloc comes from libCEED's own device-side handling of the contact data, not the QFunction itself. It runs either way correctly; the contact operators just fall back to a normal apply rather than being replayed.

This is a pretty big red flag to me. The Nitsche contact operators don't actually do anything out of the ordinary on the libCEED side. Them failing to work means that something about this setup (very possibly something with passive inputs and outputs) is violating the libCEED interface assumptions.

@Nafees01

Copy link
Copy Markdown
Author

Sub-operators that cannot be captured, for example, contact operators that allocate memory (cudaMalloc) during apply

Are there specific operators you were unable to use in Ratel? None of the QFunctions there should alloc during operator application.

The only example where the contact (Nitsche) sub-operator couldn't be captured was the quasistatic contact case (ex02-quasistatic-elasticity-linear-platen). Everything else I tried static, dynamic, multi-material, neo-Hookean, Mooney-Rivlin captured and replayed fine.
And you're right that the QFunction doesn't allocate; the cudaMalloc comes from libCEED's own device-side handling of the contact data, not the QFunction itself. It runs either way correctly; the contact operators just fall back to a normal apply rather than being replayed.

This is a pretty big red flag to me. The Nitsche contact operators don't actually do anything out of the ordinary on the libCEED side. Them failing to work means that something about this setup (very possibly something with passive inputs and outputs) is violating the libCEED interface assumptions.

Thanks for flagging this. On re-checking linear-platen with CEED_DEBUG=1, the contact operators appear to fail earlier than graph capture: cuda-gen hits an NVRTC compile error (GradTransposeTensor3dFlattened defined twice in cuda-shared-basis-tensor-flattened-templates.h) and then falls back to /gpu/cuda/ref. So this looks like a pre-existing cuda-gen JIT issue triggered by the contact operator's basis configuration, rather than something specific to Nitsche or CUDA graphs.

@zatkins-dev

Copy link
Copy Markdown
Collaborator

On re-checking linear-platen with CEED_DEBUG=1, the contact operators appear to fail earlier than graph capture: cuda-gen hits an NVRTC compile error (GradTransposeTensor3dFlattened defined twice in cuda-shared-basis-tensor-flattened-templates.h) and then falls back to /gpu/cuda/ref

Sweet, good catch. I'll fix that separately.

Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c
Comment thread backends/cuda-ref/ceed-cuda-ref-qfunctioncontext.c
Comment thread backends/cuda-ref/ceed-cuda-ref-vector.c
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread interface/ceed-cuda.c Outdated
Comment thread interface/ceed-cuda.c Outdated
Comment thread interface/ceed-cuda.c Outdated
@jeremylt
jeremylt requested a review from zatkins-dev July 28, 2026 06:08
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c Outdated
Comment thread backends/cuda-gen/ceed-cuda-gen-operator.c
cudaStreamIsCapturing(cudaStreamPerThread, &capture_status);
if (capture_status != cudaStreamCaptureStatusNone) {
// During capture, use async memset with cudaStreamPerThread
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemsetAsync(impl->d_array, 0, length * sizeof(CeedScalar), cudaStreamPerThread));

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.

Note to me and @jeremylt -- we should probably make these async/non-blocking during normal execution; it shouldn't affect determinism and will likely improve perf. If blocking is needed, that's likely a bug anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think honestly than would be cleaner and clearer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yea, the more I think on it, the better off I think we are just defaulting to async and skipping this special case logic

@zatkins-dev zatkins-dev 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.

I think this looks pretty good. I'll hold off for @jeremylt on final approval, but this is a really cool feature add! Thanks @Nafees01!

@Nafees01

Nafees01 commented Aug 3, 2026

Copy link
Copy Markdown
Author

I think this looks pretty good. I'll hold off for @jeremylt on final approval, but this is a really cool feature add! Thanks @Nafees01!

Thanks @zatkins-dev and thanks @jeremylt for all the detailed feedback - really helped get this in good shape. Appreciate you both taking the time!

if (output_vec != CEED_VECTOR_NONE) CeedCallBackend(CeedVectorGetArray(output_vec, CEED_MEM_DEVICE, &output_arr));
CeedCallBackend(CeedOperatorApplyAddCore_Cuda_gen(op, NULL, input_arr, output_arr, &is_run_good, request));
enum cudaStreamCaptureStatus capture_status;
cudaStreamIsCapturing(cudaStreamPerThread, &capture_status);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

call should be wrapped in error catching macro

CeedCallBackend(CeedOperatorApplyAddCore_Cuda_gen(op, NULL, input_arr, output_arr, &is_run_good, request));
enum cudaStreamCaptureStatus capture_status;
cudaStreamIsCapturing(cudaStreamPerThread, &capture_status);
CUstream stream_to_use = (capture_status != cudaStreamCaptureStatusNone) ? cudaStreamPerThread : NULL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
CUstream stream_to_use = (capture_status != cudaStreamCaptureStatusNone) ? cudaStreamPerThread : NULL;
CUstream stream_to_use = (capture_status != cudaStreamCaptureStatusNone) ? cudaStreamPerThread : NULL;

Always blank line after variable declaration

CeedOperatorField *op_input_fields, *op_output_fields;
CeedQFunction qf = NULL;
CeedQFunctionField *qf_input_fields;
void *d_c = NULL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

variable name should use words

CeedOperator *sub_operators;
CeedInt num_suboperators;

ceed = CeedOperatorReturnCeed(op);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This function should not be used to get a persistent ceed

Suggested change
ceed = CeedOperatorReturnCeed(op);
CeedCallBackend(CeedOperatorGetCeed(op, &ceed));

CeedCallBackend(CeedOperatorSetEnableCudaGraph(op, false));
return CeedOperatorApplyAddComposite_NoGraph_Cuda_gen(op, input_vec, output_vec, request);
}
return CEED_ERROR_SUCCESS;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And destroy that Ceed down here

@jeremylt jeremylt Aug 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

really, before each of the return statements

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could, alternatively, use CeedOperatorReturnCeed() exactly in the sites where you use the ceed. Just don't use CeedOperatorReturnCeed() to get around having to destroy a persistent ceed variable

Comment on lines +1097 to +1098

CeedCallBackend(CeedOperatorIsComposite(op, &is_composite));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
CeedCallBackend(CeedOperatorIsComposite(op, &is_composite));
CeedCallBackend(CeedOperatorIsComposite(op, &is_composite));

CeedOperatorLinearAssembleAddDiagonalAtPoints_Cuda_gen));
CeedCallBackend(CeedSetBackendFunction(ceed, "Operator", op, "LinearAssembleSingle", CeedOperatorAssembleSingleAtPoints_Cuda_gen));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ok, either add blank lines before those if/else blocks or don't please - the mix is confusing

Comment on lines +29 to +30

bool use_graph;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
bool use_graph;
// Graph capture data
bool use_graph;

Either a comment here or no blank line

Comment on lines +329 to +339
// Check if we're in CUDA Graph capture mode
enum cudaStreamCaptureStatus capture_status;

cudaStreamIsCapturing(cudaStreamPerThread, &capture_status);
if (capture_status != cudaStreamCaptureStatusNone) {
// During capture, use async memset with cudaStreamPerThread
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemsetAsync(impl->d_array, 0, length * sizeof(CeedScalar), cudaStreamPerThread));
} else {
// Normal execution, use blocking memset
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemset(impl->d_array, 0, length * sizeof(CeedScalar)));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Check if we're in CUDA Graph capture mode
enum cudaStreamCaptureStatus capture_status;
cudaStreamIsCapturing(cudaStreamPerThread, &capture_status);
if (capture_status != cudaStreamCaptureStatusNone) {
// During capture, use async memset with cudaStreamPerThread
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemsetAsync(impl->d_array, 0, length * sizeof(CeedScalar), cudaStreamPerThread));
} else {
// Normal execution, use blocking memset
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemset(impl->d_array, 0, length * sizeof(CeedScalar)));
}
CeedCallCuda(CeedVectorReturnCeed(vec), cudaMemsetAsync(impl->d_array, 0, length * sizeof(CeedScalar), cudaStreamPerThread));

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