Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions libs/qec/include/cudaq/qec/realtime/ai_predecoder_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@ class ai_predecoder_service : public ai_decoder_service {

virtual ~ai_predecoder_service();

void capture_graph(cudaStream_t stream, bool device_launch);
/// @param stream CUDA stream to use for capture and warm-up inference.
/// @param device_launch If true, instantiate the graph for device launch.
/// @param save_graph If true, retain a clone of the captured CUDA graph
/// template so it can be inspected later via
/// get_captured_graph() (e.g. by the free functions in
/// cudaq/qec/realtime/graph_resources.h). Default is false; the
/// service otherwise destroys the template immediately after
/// instantiation.
void capture_graph(cudaStream_t stream, bool device_launch,
bool save_graph = false);
void capture_graph(cudaStream_t stream) override {
capture_graph(stream, true);
capture_graph(stream, true, false);
}

bool poll_next_job(pre_decoder_job &out_job);
Expand All @@ -78,6 +87,13 @@ class ai_predecoder_service : public ai_decoder_service {

void **get_host_ring_ptrs() const { return h_ring_ptrs_; }

/// @brief Return the retained clone of the captured graph template.
/// @details Non-null only when capture_graph() was called with
/// save_graph=true. Ownership stays with this service; callers must
/// NOT destroy the returned handle. Intended for opt-in introspection
/// (e.g. cudaq::qec::realtime::experimental::collect_graph_resources).
cudaGraph_t get_captured_graph() const { return captured_graph_; }

private:
/// Passthrough constructor (delegates to base passthrough constructor).
ai_predecoder_service(void **device_mailbox_slot, int queue_depth,
Expand All @@ -92,6 +108,12 @@ class ai_predecoder_service : public ai_decoder_service {
cuda::atomic<int, cuda::thread_scope_system> *d_ready_flags_ = nullptr;
void **d_ring_ptrs_ = nullptr;
void *d_predecoder_outputs_ = nullptr;

/// Optional clone of the captured cudaGraph_t template, retained only
/// when capture_graph() was called with save_graph=true. Destroyed in
/// the destructor. The instantiated graph_exec_ lives on the base
/// class ai_decoder_service.
cudaGraph_t captured_graph_ = nullptr;
};

} // namespace cudaq::qec::realtime::experimental
61 changes: 45 additions & 16 deletions libs/qec/include/cudaq/qec/realtime/graph_resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,53 @@

#pragma once

#include <cstdint>
#include <cuda_runtime.h>
#include <iosfwd>
#include <string>
#include <vector>

namespace cudaq::qec::realtime {
namespace cudaq::qec::realtime::experimental {

/// Resources returned by decoder::capture_decode_graph().
///
/// The decoder plugin captures a CUDA graph internally and populates this
/// struct. The host dispatcher (libcudaq-realtime-host-dispatch) uses
/// graph_exec / stream to launch the graph, and writes per-slot I/O
/// addresses into h_mailbox before each launch. function_id is used by
/// the host dispatcher to route RPC requests to the correct graph worker.
struct graph_resources {
cudaGraphExec_t graph_exec = nullptr;
cudaStream_t stream = nullptr;
void **d_mailbox = nullptr; ///< device-mapped pinned pointer
void **h_mailbox = nullptr; ///< host pointer to same pinned memory
uint32_t function_id = 0;
/// @brief Per-kernel resource usage captured from a CUDA graph.
struct kernel_resource_info {
std::string name; ///< Kernel symbol name (demangled if available).
dim3 grid_dim; ///< Grid dimensions from the graph node.
dim3 block_dim; ///< Block dimensions from the graph node.
std::size_t static_shmem = 0; ///< Static shared memory per block (bytes).
std::size_t dynamic_shmem = 0; ///< Dynamic shared memory per block (bytes).
std::size_t local_mem = 0; ///< Local memory per thread (bytes).
std::size_t const_mem = 0; ///< Constant memory used by the kernel (bytes).
int num_regs = 0; ///< Registers per thread.
int max_threads_per_block = 0; ///< Hardware max threads for this kernel.
};

/// @brief Aggregate resource usage for a CUDA graph.
struct graph_resource_info {
std::size_t total_nodes = 0;
std::size_t kernel_nodes = 0;
std::size_t memcpy_nodes = 0;
std::size_t host_nodes = 0;
std::size_t other_nodes = 0;
std::vector<kernel_resource_info> kernels;
};

} // namespace cudaq::qec::realtime
/// @brief Walk a captured CUDA graph and return per-kernel resource usage.
/// @param graph A captured (not-yet-destroyed) CUDA graph handle.
/// @returns An empty graph_resource_info if @p graph is null or traversal
/// fails, otherwise populated aggregate + per-kernel info.
///
/// @warning This routine uses the CUDA driver API
/// (@c cuGraphKernelNodeGetParams, @c cuFuncGetAttribute, @c cuFuncGetName)
/// to introspect kernels launched by external libraries such as TensorRT.
/// Those calls perturb the primary CUDA context state and can interfere
/// with DOCA / GPU-RoCE setup on the FPGA bridge path. Callers that
/// share a CUDA context with DOCA-based transports must NOT invoke this
/// function.
graph_resource_info collect_graph_resources(cudaGraph_t graph);

/// @brief Pretty-print graph resource usage to an output stream.
/// @param os Output stream (e.g. @c std::cout).
/// @param info Collected info from @c collect_graph_resources.
void print_graph_resources(std::ostream &os, const graph_resource_info &info);

} // namespace cudaq::qec::realtime::experimental
19 changes: 17 additions & 2 deletions libs/qec/lib/realtime/ai_predecoder_service.cu
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

#include "cudaq/qec/realtime/ai_predecoder_service.h"
#include "cudaq/qec/realtime/nvtx_helpers.h"
#include <cstdlib>
#include <cuda/atomic>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -123,10 +122,14 @@ ai_predecoder_service::~ai_predecoder_service() {
cudaFreeHost(h_predecoder_outputs_);
h_predecoder_outputs_ = nullptr;
}
if (captured_graph_) {
cudaGraphDestroy(captured_graph_);
captured_graph_ = nullptr;
}
}

void ai_predecoder_service::capture_graph(cudaStream_t stream,
bool device_launch) {
bool device_launch, bool save_graph) {
bool has_trt = (context_ != nullptr);

if (has_trt) {
Expand Down Expand Up @@ -161,6 +164,18 @@ void ai_predecoder_service::capture_graph(cudaStream_t stream,

SERVICE_CUDA_CHECK(cudaStreamEndCapture(stream, &graph));

// Optionally retain a clone of the captured graph template for opt-in
// introspection (see cudaq/qec/realtime/graph_resources.h). This
// service otherwise destroys the template immediately after
// instantiation -- the graph_exec_ is all that's needed at runtime.
if (save_graph) {
if (captured_graph_) {
cudaGraphDestroy(captured_graph_);
captured_graph_ = nullptr;
}
SERVICE_CUDA_CHECK(cudaGraphClone(&captured_graph_, graph));
}

if (device_launch) {
cudaError_t inst_err = cudaGraphInstantiateWithFlags(
&graph_exec_, graph, cudaGraphInstantiateFlagDeviceLaunch);
Expand Down
197 changes: 197 additions & 0 deletions libs/qec/lib/realtime/graph_resources.cu

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 this be a .cpp file since there isn't any real CUDA code in here?

Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*******************************************************************************
* Copyright (c) 2026 NVIDIA Corporation & Affiliates. *
* All rights reserved. *
* *
* This source code and the accompanying materials are made available under *
* the terms of the Apache License 2.0 which accompanies this distribution. *
******************************************************************************/

#include "cudaq/qec/realtime/graph_resources.h"

#include <cstdlib>
#include <cuda.h>
#include <cuda_runtime.h>
#include <cxxabi.h>
#include <ostream>
#include <string>
#include <utility>
#include <vector>

namespace cudaq::qec::realtime::experimental {

namespace {

std::string demangle_symbol(const char *mangled) {
if (!mangled)
return "<unknown>";
int status = 0;
char *out = abi::__cxa_demangle(mangled, nullptr, nullptr, &status);
std::string name = (status == 0 && out) ? std::string(out) : mangled;
std::free(out);
return name;
}

} // namespace

graph_resource_info collect_graph_resources(cudaGraph_t graph) {
graph_resource_info result{};
if (!graph)
return result;

std::size_t num_nodes = 0;
if (cudaGraphGetNodes(graph, nullptr, &num_nodes) != cudaSuccess ||
num_nodes == 0)
return result;

std::vector<cudaGraphNode_t> nodes(num_nodes);
if (cudaGraphGetNodes(graph, nodes.data(), &num_nodes) != cudaSuccess)
return result;

result.total_nodes = num_nodes;

for (auto node : nodes) {
cudaGraphNodeType type;
if (cudaGraphNodeGetType(node, &type) != cudaSuccess)
continue;

switch (type) {
case cudaGraphNodeTypeKernel:
++result.kernel_nodes;
break;
case cudaGraphNodeTypeMemcpy:
++result.memcpy_nodes;
continue;
case cudaGraphNodeTypeHost:
++result.host_nodes;
continue;
default:
++result.other_nodes;
continue;
}

kernel_resource_info info{};

// Try runtime API first (works for kernels launched via <<<>>>).
cudaKernelNodeParams params{};
if (cudaGraphKernelNodeGetParams(node, &params) == cudaSuccess) {
info.grid_dim = params.gridDim;
info.block_dim = params.blockDim;
info.dynamic_shmem = params.sharedMemBytes;

const char *mangled = nullptr;
if (params.func)
cudaFuncGetName(&mangled, params.func);
info.name = demangle_symbol(mangled);

cudaFuncAttributes attr{};
if (params.func &&
cudaFuncGetAttributes(&attr, params.func) == cudaSuccess) {
info.static_shmem = attr.sharedSizeBytes;
info.local_mem = attr.localSizeBytes;
info.const_mem = attr.constSizeBytes;
info.num_regs = attr.numRegs;
info.max_threads_per_block = attr.maxThreadsPerBlock;
}
} else {
// Fall back to driver API for TRT-internal kernels launched via
// cuLaunchKernel. WARNING: these driver calls perturb CUDA context
// state in ways that interfere with DOCA/Hololink GPU-RoCE setup, so
// callers that share a CUDA context with DOCA-based transports must
// NOT invoke this function.
CUDA_KERNEL_NODE_PARAMS drv_params{};
if (cuGraphKernelNodeGetParams(reinterpret_cast<CUgraphNode>(node),
&drv_params) == CUDA_SUCCESS) {
info.grid_dim =
dim3(drv_params.gridDimX, drv_params.gridDimY, drv_params.gridDimZ);
info.block_dim = dim3(drv_params.blockDimX, drv_params.blockDimY,
drv_params.blockDimZ);
info.dynamic_shmem = drv_params.sharedMemBytes;

CUfunction func = drv_params.func;
if (func) {
const char *raw_name = nullptr;
if (cuFuncGetName(&raw_name, func) == CUDA_SUCCESS)
info.name = demangle_symbol(raw_name);

int regs = 0;
if (cuFuncGetAttribute(&regs, CU_FUNC_ATTRIBUTE_NUM_REGS, func) ==
CUDA_SUCCESS)
info.num_regs = regs;

int sshmem = 0;
if (cuFuncGetAttribute(&sshmem, CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES,
func) == CUDA_SUCCESS)
info.static_shmem = static_cast<std::size_t>(sshmem);

int lmem = 0;
if (cuFuncGetAttribute(&lmem, CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES,
func) == CUDA_SUCCESS)
info.local_mem = static_cast<std::size_t>(lmem);

int cmem = 0;
if (cuFuncGetAttribute(&cmem, CU_FUNC_ATTRIBUTE_CONST_SIZE_BYTES,
func) == CUDA_SUCCESS)
info.const_mem = static_cast<std::size_t>(cmem);

int max_threads = 0;
if (cuFuncGetAttribute(&max_threads,
CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK,
func) == CUDA_SUCCESS)
info.max_threads_per_block = max_threads;
}
if (info.name.empty())
info.name = "<unknown-driver-kernel>";
} else {
info.name = "<introspection-failed>";
}
}

result.kernels.push_back(std::move(info));
}

return result;
}

void print_graph_resources(std::ostream &os, const graph_resource_info &g) {
os << "[GraphResources] total_nodes=" << g.total_nodes
<< " kernels=" << g.kernel_nodes << " memcpy=" << g.memcpy_nodes
<< " host=" << g.host_nodes << " other=" << g.other_nodes << "\n";

std::size_t total_regs_per_launch = 0;
std::size_t total_shmem_per_launch = 0;
std::size_t total_threads = 0;

for (std::size_t i = 0; i < g.kernels.size(); ++i) {
const auto &k = g.kernels[i];
std::size_t blocks =
static_cast<std::size_t>(k.grid_dim.x) * k.grid_dim.y * k.grid_dim.z;
std::size_t threads_per_block =
static_cast<std::size_t>(k.block_dim.x) * k.block_dim.y * k.block_dim.z;
std::size_t launch_threads = blocks * threads_per_block;
std::size_t launch_regs =
launch_threads * static_cast<std::size_t>(k.num_regs);
std::size_t launch_shmem = blocks * (k.static_shmem + k.dynamic_shmem);

total_regs_per_launch += launch_regs;
total_shmem_per_launch += launch_shmem;
total_threads += launch_threads;

os << " [" << i << "] " << k.name << "\n"
<< " grid=(" << k.grid_dim.x << "," << k.grid_dim.y << ","
<< k.grid_dim.z << ") block=(" << k.block_dim.x << "," << k.block_dim.y
<< "," << k.block_dim.z << ")"
<< " threads=" << launch_threads << "\n"
<< " regs/thread=" << k.num_regs << " local/thread=" << k.local_mem
<< "B"
<< " shmem/block=" << (k.static_shmem + k.dynamic_shmem)
<< "B (static=" << k.static_shmem << " dynamic=" << k.dynamic_shmem
<< ")"
<< " max_threads_per_block=" << k.max_threads_per_block << "\n";
}

os << " Total launch: threads=" << total_threads
<< " regs=" << total_regs_per_launch << " shmem=" << total_shmem_per_launch
<< "B\n";
}

} // namespace cudaq::qec::realtime::experimental
3 changes: 2 additions & 1 deletion libs/qec/unittests/realtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if(TENSORRT_INCLUDE_DIR AND TENSORRT_LIBRARY AND TENSORRT_ONNX_PARSER_LIBRARY
predecoder_pipeline_common.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/realtime/ai_decoder_service.cu
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/realtime/ai_predecoder_service.cu
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/realtime/graph_resources.cu
)

set_target_properties(test_realtime_predecoder_w_pymatching PROPERTIES
Expand All @@ -51,6 +52,7 @@ if(TENSORRT_INCLUDE_DIR AND TENSORRT_LIBRARY AND TENSORRT_ONNX_PARSER_LIBRARY

target_link_libraries(test_realtime_predecoder_w_pymatching PRIVATE
CUDA::cudart
CUDA::cuda_driver
${TENSORRT_LIBRARY}
${TENSORRT_ONNX_PARSER_LIBRARY}
${CUDAQ_REALTIME_LIBRARY}
Expand Down Expand Up @@ -143,7 +145,6 @@ if (GPU_ROCE_TRANSCEIVER_LIB AND CUDAQ_REALTIME_INCLUDE_DIR AND
$<$<BOOL:${_CUDAQ_LIBRARY}>:${_CUDAQ_LIBRARY}>
$<$<BOOL:${_NVQIR_LIBRARY}>:${_NVQIR_LIBRARY}>
CUDA::cudart
CUDA::cuda_driver
${DOCA_VERBS_LIB}
${DOCA_GPUNETIO_LIB}
${DOCA_COMMON_LIB}
Expand Down
Loading
Loading