// Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "paddle/phi/backends/gpu/cuda/cuda_graph.h" #include "glog/logging.h" #include "paddle/common/flags.h" #ifdef PADDLE_WITH_CUDA COMMON_DECLARE_bool(use_cuda_malloc_async_allocator); COMMON_DECLARE_bool(auto_free_cudagraph_allocations_on_launch); namespace phi::backends::gpu { std::unique_ptr CUDAGraph::capturing_graph_{nullptr}; paddle::optional CUDAGraph::capturing_thread_id_{paddle::none}; std::vector> CUDAGraph::cudagraph_pre_capture_callbacks_; static std::vector ToposortCUDAGraph(cudaGraph_t graph) { size_t num_nodes; PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphGetNodes(graph, nullptr, &num_nodes)); std::vector nodes(num_nodes); PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetNodes(graph, nodes.data(), &num_nodes)); size_t num_edges; #if CUDA_VERSION < 13000 PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetEdges(graph, nullptr, nullptr, &num_edges)); std::vector from(num_edges), to(num_edges); PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetEdges(graph, from.data(), to.data(), &num_edges)); #else PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetEdges(graph, nullptr, nullptr, nullptr, &num_edges)); std::vector from(num_edges), to(num_edges); PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetEdges(graph, from.data(), to.data(), nullptr, &num_edges)); #endif std::unordered_map> in_edges, out_edges; for (auto node : nodes) { in_edges[node]; out_edges[node]; } for (size_t i = 0; i < num_edges; ++i) { in_edges[to[i]].insert(from[i]); out_edges[from[i]].insert(to[i]); } std::queue q; for (const auto &pair : in_edges) { if (pair.second.empty()) { q.push(pair.first); } } nodes.clear(); while (!q.empty()) { auto cur = q.front(); q.pop(); nodes.push_back(cur); for (auto out_node : out_edges.at(cur)) { auto &in_nodes = in_edges.at(out_node); in_nodes.erase(cur); if (in_nodes.empty()) { q.push(out_node); } } } PADDLE_ENFORCE_EQ( nodes.size(), num_nodes, common::errors::InvalidArgument("Toposort error, this may be a bug.")); return nodes; } CUDAGraphID CUDAGraph::UniqueID() { static std::atomic id; return id.fetch_add(1); } int64_t CUDAGraph::UniqueMemoryPoolID() { static std::atomic id(CUDAGraph::kDefaultPoolID + 1); return id.fetch_add(1); } void CUDAGraph::Reset() { if (is_reset_) return; for (auto graph : graphs_) { PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphDestroy(graph)); } graphs_.clear(); for (auto exec_graph : exec_graphs_) { PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphExecDestroy(exec_graph)); } exec_graphs_.clear(); // callback should be called in reverse order because the latter added // callback may rely on the former added callback. for (auto iter = cudagraph_post_reset_callbacks_.rbegin(); iter != cudagraph_post_reset_callbacks_.rend(); ++iter) { (*iter)(*this); } cudagraph_post_reset_callbacks_.clear(); is_reset_ = true; } void CUDAGraph::Replay() { is_replayed_ = true; PADDLE_ENFORCE_EQ(is_reset_, false, common::errors::PermissionDenied( "Cannot replay the CUDA Graph after reset is called.")); size_t n = exec_graphs_.size(); for (size_t i = 0; i < n; ++i) { if (!is_first_run_) { for (auto &hook : cudagraph_pre_replay_callbacks_[i]) { hook(exec_graphs_[i]); } } PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphLaunch(exec_graphs_[i], stream_)); } is_first_run_ = false; } void CUDAGraph::BeginSegmentCapture() { ThrowErrorIfNotSupportCUDAGraph(); PADDLE_ENFORCE_EQ(IsCapturing(), true, common::errors::PermissionDenied( "BeginSegmentCapture should be called when CUDA " "Graph is capturing.")); if (IsThreadLocalCapturing()) { PADDLE_ENFORCE_EQ(IsThisThreadCapturing(), true, common::errors::PermissionDenied( "When capturing CUDA Graph in the thread local mode, " "you cannot begin segmented capturing in the thread " "which is not the one that starts the capturing.")); } for (auto &hook : cudagraph_pre_capture_callbacks_) { hook(); } PADDLE_ENFORCE_GPU_SUCCESS(cudaStreamBeginCapture( capturing_graph_->stream_, capturing_graph_->capture_mode_)); PADDLE_ENFORCE_EQ(IsValidCapturing(), true, common::errors::PermissionDenied( "CUDA Graph should not be invalidated.")); VLOG(10) << "Begin to capture CUDA Graph with ID " << capturing_graph_->id_ << ", segment id " << capturing_graph_->graphs_.size() << ", memory pool id " << capturing_graph_->pool_id_; } void CUDAGraph::BeginCapture(phi::GPUPlace place, cudaStream_t stream, cudaStreamCaptureMode mode, bool enable_replace) { ThrowErrorIfNotSupportCUDAGraph(); PADDLE_ENFORCE_EQ(IsCapturing(), false, common::errors::PermissionDenied( "CUDA Graph can only captured one by one.")); PADDLE_ENFORCE_NOT_NULL( stream, common::errors::PermissionDenied( "CUDA Graph cannot be captured in default CUDA stream 0.")); capturing_graph_.reset(new CUDAGraph(enable_replace)); capturing_graph_->place_ = place; capturing_graph_->stream_ = stream; capturing_graph_->capture_mode_ = mode; if (mode == cudaStreamCaptureModeThreadLocal) { capturing_thread_id_ = std::this_thread::get_id(); VLOG(10) << "Capturing CUDA Graph in thread local mode, thread id: " << capturing_thread_id_; } BeginSegmentCapture(); } inline void sync_streams(gpuStream_t to_record, gpuStream_t to_wait) { if (to_record == to_wait) return; cudaEvent_t event = nullptr; PADDLE_ENFORCE_GPU_SUCCESS( cudaEventCreateWithFlags(&event, cudaEventDisableTiming)); PADDLE_ENFORCE_GPU_SUCCESS(cudaEventRecord(event, to_record)); PADDLE_ENFORCE_GPU_SUCCESS(cudaStreamWaitEvent(to_wait, event)); PADDLE_ENFORCE_GPU_SUCCESS(cudaEventDestroy(event)); } void CUDAGraph::EndSegmentCapture() { ThrowErrorIfNotSupportCUDAGraph(); PADDLE_ENFORCE_EQ( IsCapturing(), true, common::errors::PermissionDenied("No CUDA Graph is capturing.")); for (const auto &stream : capturing_graph_->streams_to_join_) { VLOG(10) << "Joining steam when the capture is going to end stream =" << stream; sync_streams(stream, capturing_graph_->stream_); } capturing_graph_->streams_to_join_.clear(); cudaGraph_t graph; PADDLE_ENFORCE_GPU_SUCCESS( cudaStreamEndCapture(capturing_graph_->stream_, &graph)); auto num_nodes = static_cast(-1); PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphGetNodes(graph, nullptr, &num_nodes)); if (num_nodes == 0) { PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphDestroy(graph)); VLOG(10) << "Skip empty CUDA Graph with ID " << capturing_graph_->id_ << ", segment id " << capturing_graph_->graphs_.size() << ", memory pool id " << capturing_graph_->pool_id_; return; } for (auto &cudagraph_post_capture_callback : capturing_graph_->cudagraph_post_capture_callbacks_) { cudagraph_post_capture_callback(); } capturing_graph_->cudagraph_post_capture_callbacks_.clear(); capturing_graph_->cudagraph_pre_replay_callbacks_.emplace_back( CUDAGraphNodeLauncher::Instance().GetParameterSettersForExecGraph(graph)); cudaGraphExec_t exec_graph; if (FLAGS_use_cuda_malloc_async_allocator && FLAGS_auto_free_cudagraph_allocations_on_launch) { #if CUDA_VERSION >= 11040 VLOG(1) << "cudaGraphInstantiateFlagAutoFreeOnLaunch is enabled!"; PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphInstantiateWithFlags( &exec_graph, graph, cudaGraphInstantiateFlagAutoFreeOnLaunch)); #else PADDLE_THROW(common::errors::Unimplemented( "The cudaGraphInstantiateFlagAutoFreeOnLaunch is only supported when " "CUDA version >= 11.4.0")); #endif } else { PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphInstantiate(&exec_graph, graph, nullptr, nullptr, 0)); } VLOG(10) << "End to capture CUDA Graph with ID " << capturing_graph_->id_ << ", segment id " << capturing_graph_->graphs_.size() << ", memory pool id " << capturing_graph_->pool_id_; capturing_graph_->graphs_.emplace_back(graph); capturing_graph_->exec_graphs_.emplace_back(exec_graph); if (capturing_graph_->enable_replace_) { capturing_graph_->CacheKernelNodeInfos(capturing_graph_->graphs_.size() - 1); } } std::unique_ptr CUDAGraph::EndCapture() { EndSegmentCapture(); capturing_thread_id_ = paddle::none; return std::move(capturing_graph_); } bool CUDAGraph::IsValidCapturing() { if (!IsCapturing()) return false; cudaStreamCaptureStatus status; CUDAGraphID id; PADDLE_ENFORCE_GPU_SUCCESS( cudaStreamGetCaptureInfo(capturing_graph_->stream_, &status, &id)); return status == cudaStreamCaptureStatusActive; } static std::string ConcatPath(const std::string &dirname, const std::string &filename) { #ifdef _WIN32 const std::array kFileSep = {"\\"}; #else const std::array kFileSep = {"/"}; #endif if (!dirname.empty() && dirname.back() == kFileSep[0]) { return dirname + filename; } else { return dirname + kFileSep.data() + filename; } } void CUDAGraph::PrintToDotFiles(const std::string &dirname, unsigned int flags) { ThrowErrorIfNotSupportCUDAGraph(); #if CUDA_VERSION >= 11030 for (size_t i = 0; i < graphs_.size(); ++i) { auto filename = ConcatPath(dirname, "segment_" + std::to_string(i) + ".dot"); VLOG(10) << "Save the " << i << "-th segment of graph " << id_ << " to " << filename; PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphDebugDotPrint(graphs_[i], filename.c_str(), flags)); } #else PADDLE_THROW(common::errors::Unimplemented( "The print_to_dot_files() method is only supported when CUDA version >= " "11.3.")); #endif } void CUDAGraph::CacheKernelNodeInfos(size_t segment_idx) { auto &graph = graphs_[segment_idx]; size_t numNodes = 0; cudaGraphGetNodes(graph, nullptr, &numNodes); std::vector nodes(numNodes); cudaGraphGetNodes(graph, nodes.data(), &numNodes); std::vector kernel_nodes; for (auto &node : nodes) { cudaGraphNodeType type; cudaGraphNodeGetType(node, &type); if (type == cudaGraphNodeTypeKernel) { KernelNodeInfo info; info.node = node; memset(&info.params, 0, sizeof(info.params)); // Use Driver API to get params (works for both Runtime and Driver API // kernels, unlike Runtime API which may return invalid kernelParams // for JIT kernels such as DeepGEMM) dynload::cuGraphKernelNodeGetParams(static_cast(node), &info.params); info.param_infos = GetKernelParamInfos(static_cast(info.params.func)); kernel_nodes.emplace_back(std::move(info)); } } VLOG(4) << "Cached " << kernel_nodes.size() << " kernel nodes for segment " << segment_idx; cached_kernel_nodes_.emplace_back(std::move(kernel_nodes)); } void CUDAGraph::ReplaceInputPtrs(const std::vector &old_ptrs, const std::vector &new_ptrs) { PADDLE_ENFORCE_EQ( enable_replace_, true, common::errors::PermissionDenied( "ReplaceInputPtrs requires enable_replace to be set to true " "when creating CUDAGraph.")); #if CUDA_VERSION >= 12040 for (size_t i = 0; i < cached_kernel_nodes_.size(); ++i) { for (auto &kernel_info : cached_kernel_nodes_[i]) { auto ¶ms = kernel_info.params; bool modified = false; for (size_t k = 0; k < kernel_info.param_infos.size(); k++) { size_t param_size = kernel_info.param_infos[k].size; char *param_base = reinterpret_cast(params.kernelParams[k]); for (size_t offset = 0; offset + sizeof(void *) <= param_size; offset += sizeof(void *)) { void *actual_val = *(reinterpret_cast(param_base + offset)); for (size_t j = 0; j < old_ptrs.size(); j++) { if (old_ptrs[j] == actual_val) { VLOG(4) << "cuda func " << params.func << " match old ptr " << actual_val << " at param " << k << " offset " << offset << ", replace with " << new_ptrs[j]; *(reinterpret_cast(param_base + offset)) = new_ptrs[j]; modified = true; break; } } } } if (modified) { dynload::cuGraphExecKernelNodeSetParams( static_cast(exec_graphs_[i]), static_cast(kernel_info.node), ¶ms); } } } #endif } std::vector CUDAGraph::GetKernelParamInfos( CUfunction func) { std::vector infos; #if CUDA_VERSION >= 12040 size_t paramOffset, paramSize; int k = 0; while (dynload::cuFuncGetParamInfo(func, k, ¶mOffset, ¶mSize) == CUDA_SUCCESS) { infos.push_back({paramOffset, paramSize}); VLOG(4) << "[GetKernelParamInfos] func " << func << " param[" << k << "] offset=" << paramOffset << " size=" << paramSize; k++; } #endif return infos; } void CUDAGraphNodeLauncher::KernelNodeLaunch( parameterSetter_t parameterSetter, gpuKernelCallback_t cudaKernelCallback) { if (UNLIKELY(phi::backends::gpu::CUDAGraph::IsThisThreadCapturing())) { unsigned int id = GenerateIdentifier(); auto cudaFunc = cudaKernelCallback(id); parameterSetters[cudaFunc][id] = parameterSetter; VLOG(10) << "[KernelNodeLaunch] Launch kernel with cudaFunc = " << cudaFunc << " id = " << id; } else { cudaKernelCallback(0); } } std::vector CUDAGraphNodeLauncher::GetParameterSettersForExecGraph(cudaGraph_t graph) { size_t num_nodes; PADDLE_ENFORCE_GPU_SUCCESS(cudaGraphGetNodes(graph, nullptr, &num_nodes)); std::vector nodes(num_nodes); PADDLE_ENFORCE_GPU_SUCCESS( cudaGraphGetNodes(graph, nodes.data(), &num_nodes)); std::vector> hooks; for (auto node : nodes) { CUgraphNode cuNode = node; CUgraphNodeType pType; PADDLE_ENFORCE_GPU_SUCCESS(dynload::cuGraphNodeGetType(cuNode, &pType)); if (pType == CU_GRAPH_NODE_TYPE_KERNEL) { CUDA_KERNEL_NODE_PARAMS cuParams; PADDLE_ENFORCE_GPU_SUCCESS( dynload::cuGraphKernelNodeGetParams(cuNode, &cuParams)); gpuKernelParams kernel_params(cuParams.kernelParams); auto kernel = parameterSetters.find(static_cast(cuParams.func)); VLOG(10) << "[GetParameterSettersForExecGraph] cuParams.func = " << cuParams.func; // There exists a parameter setter if (kernel != parameterSetters.end()) { auto launchSequence = kernel->second; unsigned int id = kernel_params.As(0); VLOG(10) << "[GetParameterSettersForExecGraph] Find launch kernel id = " << id; auto parameterSetter = launchSequence.find(id); if (parameterSetter != launchSequence.end()) { auto setter = parameterSetter->second; hooks.emplace_back([setter, cuNode, cuParams]( cudaGraphExec_t exec_graph) { gpuKernelParams kernel_params(cuParams.kernelParams); setter(kernel_params); PADDLE_ENFORCE_GPU_SUCCESS(dynload::cuGraphExecKernelNodeSetParams( static_cast(exec_graph), cuNode, &cuParams)); }); } else { PADDLE_THROW(common::errors::InvalidArgument( "Error: does not find launch id")); } } } } return hooks; } } // namespace phi::backends::gpu #endif