Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .ci/scripts/wheel/test_shared_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@
"aoti_torch_cuda_randint_low_out",
"aoti_torch_cuda_sort_stable",
"aoti_torch_cuda_guard_set_index",
"aoti_torch_cuda_index_Tensor",
"aoti_torch_index_put_out",
)

_QUANTIZED_KERNEL_SYMBOLS = (
Expand Down
13 changes: 13 additions & 0 deletions backends/aoti/common_shims_slim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ AOTITorchError aoti_torch_get_sizes(Tensor* tensor, int64_t** ret_sizes) {
return Error::Ok;
}

AOTITorchError
aoti_torch_get_size(Tensor* tensor, int64_t d, int64_t* ret_size) {
if (tensor == nullptr || ret_size == nullptr) {
return Error::InvalidArgument;
}
const auto ndim = static_cast<int64_t>(tensor->dim());
if (d < -ndim || d >= ndim) {
return Error::InvalidArgument;
}
*ret_size = tensor->size(d);
return Error::Ok;
}

AOTITorchError aoti_torch_get_strides(Tensor* tensor, int64_t** ret_strides) {
if (tensor == nullptr || ret_strides == nullptr) {
return Error::InvalidArgument;
Expand Down
5 changes: 5 additions & 0 deletions backends/aoti/common_shims_slim.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ aoti_torch_get_data_ptr(Tensor* tensor, void** ret_data_ptr);
AOTI_SHIM_EXPORT AOTITorchError
aoti_torch_get_sizes(Tensor* tensor, int64_t** ret_sizes);

// Generated code reads the size of a result whose shape depends on the data
// through this, one dim at a time.
AOTI_SHIM_EXPORT AOTITorchError
aoti_torch_get_size(Tensor* tensor, int64_t d, int64_t* ret_size);

AOTI_SHIM_EXPORT AOTITorchError
aoti_torch_get_strides(Tensor* tensor, int64_t** ret_strides);

Expand Down
4 changes: 2 additions & 2 deletions backends/cuda/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ set(_aoti_cuda_shim_sources runtime/shims/memory.cpp

# Only build CUDA-only operator shims when the CUDA toolchain is available.
if(NOT EXECUTORCH_BUILD_ROCM AND CMAKE_CUDA_COMPILER)
list(APPEND _aoti_cuda_shim_sources runtime/shims/int4mm.cu
runtime/shims/sort.cu runtime/shims/rand.cu
list(APPEND _aoti_cuda_shim_sources runtime/shims/index.cu
runtime/shims/int4mm.cu runtime/shims/sort.cu runtime/shims/rand.cu
)
endif()

Expand Down
2 changes: 2 additions & 0 deletions backends/cuda/cuda_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ def get_supported_fallback_kernels(cls) -> Dict[str, Any]:
"at::_ops::sort_stable::call": None,
"aoti_torch_cuda_sort_stable": None,
"aoti_torch_cuda_randint_low_out": None,
"aoti_torch_cuda_index_Tensor": None,
"aoti_torch_index_put_out": None,
}

@classmethod
Expand Down
4 changes: 4 additions & 0 deletions backends/cuda/passes/move_cond_predicate_to_cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ def forward(self, hidden_states, encoder_hidden_states):
In this example if we keep `self.initialized` on GPU, we will need to copy it to CPU for every forward pass.
We move the predicate to CPU to avoid device to host copies.
This pass is only applicable to models that use torch.cond and its predicate is a constant buffer.

A method this pass changes keeps a constant in host memory, so the CUDA backend runs it
without a CUDA graph even when one is requested: a graph would keep the branch the capture
call took.
"""

requires_exported_program = True
Expand Down
Binary file modified backends/cuda/runtime/aoti_cuda_shims.lib
Binary file not shown.
196 changes: 135 additions & 61 deletions backends/cuda/runtime/cuda_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,28 +87,6 @@ constexpr char kUseSharedCudaStream[] = "use_shared_cuda_stream";
constexpr char kEnableCudaGraphForMethod[] = "enable_cuda_graph_for_method";
constexpr int kCudaGraphWarmupSteps = 3;
constexpr char kWeightSharingAcrossMethods[] = "weight_sharing_across_methods";

// A CUDA graph replay copies an output's raw bytes into its ETensor, which is
// right only when every element sits at the same offset in both. Size-1 dims
// never move an element, so their strides may differ.
bool output_is_raw_copyable(const SlimTensor* output, const Tensor* et_output) {
if (!output->is_contiguous() ||
output->dim() != static_cast<size_t>(et_output->dim())) {
return false;
}
// The ETensor still has its previous shape here, so its strides can be stale
// for dynamic shapes; the default dim order is enough on its own.
if (executorch::runtime::is_contiguous_dim_order(
et_output->dim_order().data(), et_output->dim())) {
return true;
}
for (size_t d = 0; d < output->dim(); d++) {
if (output->size(d) != 1 && output->stride(d) != et_output->strides()[d]) {
return false;
}
}
return true;
}
} // anonymous namespace

bool CudaGraphState::start_warmup() {
Expand Down Expand Up @@ -636,7 +614,18 @@ class ET_EXPERIMENTAL CudaBackend final
// Initialize CUDA graph state if enabled for this method. Not on a device
// without memory pools, where the method runs without a graph.
if (should_use_cuda_graph_for_method(method_name)) {
if (handle->cuda_graph_state.start_warmup()) {
const std::string host_constants = describe_host_constants(handle);
if (!host_constants.empty()) {
// A graph records only the device work of the capture call. Host code,
// such as the choice of a torch.cond branch on a flag in host memory,
// would be fixed at its value from that call.
ET_LOG(
Info,
"CUDA graph requested for method '%s', but %s; running without a "
"CUDA graph",
method_name.c_str(),
host_constants.c_str());
} else if (handle->cuda_graph_state.start_warmup()) {
ET_LOG(
Info,
"CUDA graph enabled for method '%s' (warmup=%d)",
Expand Down Expand Up @@ -790,14 +779,26 @@ class ET_EXPERIMENTAL CudaBackend final
// Copy new input data (GPU-resident) into static input buffers (D2D)
for (size_t i = 0; i < n_inputs; i++) {
auto* et_input = &(args[i]->toTensor());
const auto sizes = et_input->sizes();
const auto& captured = handle->cuda_graph_state.static_input_sizes[i];
ET_CHECK_OR_RETURN_ERROR(
et_input->nbytes() ==
handle->cuda_graph_state.static_input_nbytes[i],
sizes.size() == captured.size(),
InvalidArgument,
"CUDA graph replay: input %zu size mismatch (expected %zu, got %zu)",
"CUDA graph replay: input %zu has %zu dims, captured with %zu",
i,
handle->cuda_graph_state.static_input_nbytes[i],
et_input->nbytes());
sizes.size(),
captured.size());
for (size_t d = 0; d < sizes.size(); d++) {
ET_CHECK_OR_RETURN_ERROR(
sizes[d] == captured[d],
InvalidArgument,
"CUDA graph replay: input %zu has size %lld in dim %zu, "
"captured with %lld",
i,
static_cast<long long>(sizes[d]),
d,
static_cast<long long>(captured[d]));
}
ET_CUDA_CHECK_OR_RETURN_ERROR(cudaMemcpyAsync(
handle->cuda_graph_state.static_input_ptrs[i],
et_input->const_data_ptr(),
Expand Down Expand Up @@ -830,13 +831,70 @@ class ET_EXPERIMENTAL CudaBackend final
return commit_offgraph_kv_step(handle, kv_step_width);
}

// ---------------------------------------------------------------
// Normal path (also used for WARMUP and CAPTURE phases)
// ---------------------------------------------------------------
bool is_capture_step =
const bool is_capture_step =
(handle->cuda_graph_state.phase == CudaGraphPhase::Warmup &&
handle->cuda_graph_state.warmup_remaining == 0);
bool graph_ran = false;
Error error =
run_once(handle, args, n_inputs, n_outputs, is_capture_step, graph_ran);
if (error != Error::Ok && is_capture_step && !graph_ran) {
// A capture records kernels without running them, so until the new graph
// runs, the inputs and any state the program updates are untouched. A
// program that keeps state in host memory, where host code does run
// during a capture, never gets here: init leaves its graph off.
ET_LOG(
Info,
"CUDA graph: capture failed for '%s'; running this call and later "
"ones without a graph",
handle->method_name.c_str());
// The capture guard turned graphs off, so later calls run eagerly too.
ET_DCHECK(handle->cuda_graph_state.phase == CudaGraphPhase::Disabled);
error = run_once(
handle,
args,
n_inputs,
n_outputs,
/*is_capture_step=*/false,
graph_ran);
}
ET_CHECK_OK_OR_RETURN_ERROR(error);
return commit_offgraph_kv_step(handle, kv_step_width);
}

private:
// Names a constant of the program in host memory, or says that the
// constants could not be listed, since unknown counts as host memory.
// Empty when every constant is on the device.
static std::string describe_host_constants(cuda::CudaDelegateHandle* handle) {
const char* unlisted = "its constants could not be listed";
if (handle->extract_constants_map == nullptr) {
return unlisted;
}
std::unordered_map<std::string, AtenTensorHandle> constants;
if (handle->extract_constants_map(
handle->container_handle,
reinterpret_cast<AOTInductorConstantMapHandle>(&constants),
/*use_inactive=*/false) != Error::Ok) {
return unlisted;
}
for (const auto& [fqn, tensor] : constants) {
if (reinterpret_cast<SlimTensor*>(tensor)->is_cpu()) {
return "constant '" + fqn + "' is in host memory";
}
}
return "";
}

// Runs the program once. On the capture step it captures the CUDA graph
// instead, then launches it once, and sets graph_ran once that launch is
// queued.
Error run_once(
cuda::CudaDelegateHandle* handle,
Span<EValue*> args,
size_t n_inputs,
size_t n_outputs,
bool is_capture_step,
bool& graph_ran) const {
// Wrrap them as SlimTensors for AOTI execution without any host<->device
// copies.
std::vector<SlimTensor*> slim_inputs(n_inputs);
Expand All @@ -863,25 +921,10 @@ class ET_EXPERIMENTAL CudaBackend final
return;
}
// Free what this attempt pinned and disable graphs for this method, so
// a capture that cannot succeed costs one error rather than one on
// every fourth call for the life of the process. Eager execution is
// correct, just slower.
for (void* ptr : state_->static_input_ptrs) {
(void)cudaFree(ptr);
}
state_->static_input_ptrs.clear();
state_->static_output_ptrs.clear();
state_->static_input_nbytes.clear();
state_->static_output_nbytes.clear();
// Same order as ~CudaGraphState: the exec depends on the graph.
if (state_->graph_exec != nullptr) {
(void)cudaGraphExecDestroy(state_->graph_exec);
state_->graph_exec = nullptr;
}
if (state_->graph != nullptr) {
(void)cudaGraphDestroy(state_->graph);
state_->graph = nullptr;
}
// a capture that cannot succeed is tried once rather than on every
// later call for the life of the process. Eager execution is correct,
// just slower.
state_->release();
state_->phase = CudaGraphPhase::Disabled;
state_->warmup_remaining = 0;
(void)cudaGetLastError();
Expand Down Expand Up @@ -925,6 +968,8 @@ class ET_EXPERIMENTAL CudaBackend final
// through the guard instead of leaking this allocation.
handle->cuda_graph_state.static_input_ptrs.push_back(static_ptr);
handle->cuda_graph_state.static_input_nbytes.push_back(nbytes);
handle->cuda_graph_state.static_input_sizes.emplace_back(
et_input->sizes().begin(), et_input->sizes().end());

ET_CUDA_CHECK_OR_RETURN_ERROR(cudaMemcpy(
static_ptr,
Expand Down Expand Up @@ -1056,11 +1101,23 @@ class ET_EXPERIMENTAL CudaBackend final
}
}

ET_CHECK_OR_RETURN_ERROR(
error == Error::Ok,
Internal,
"AOTInductorModelContainerRun failed with error code %d",
error);
if (error != Error::Ok) {
// A failed capture is retried without a graph, which reports a real
// failure at Error.
if (is_capture_step) {
ET_LOG(
Info,
"AOTInductorModelContainerRun failed during CUDA graph capture "
"with error code %d",
error);
} else {
ET_LOG(
Error,
"AOTInductorModelContainerRun failed with error code %d",
error);
}
return Error::Internal;
}

if (is_capture_step) {
// End capture → instantiate graph
Expand All @@ -1083,13 +1140,28 @@ class ET_EXPERIMENTAL CudaBackend final
slim_outputs[i], &(args[i + n_inputs]->toTensor()))) {
std::fill(slim_outputs.begin(), slim_outputs.end(), nullptr);
ET_LOG(
Error,
Info,
"CUDA graph: output %zu of '%s' is not laid out like its "
"ETensor; this call fails and later calls run without a graph",
"ETensor",
i,
handle->method_name.c_str());
return Error::NotSupported;
}
// Replay copies raw bytes and never resizes, so the ETensor takes the
// captured shape here and keeps it for every replay.
const Error resize_err =
executorch::ET_RUNTIME_NAMESPACE::resize_tensor(
args[i + n_inputs]->toTensor(), slim_outputs[i]->sizes());
if (resize_err != Error::Ok) {
std::fill(slim_outputs.begin(), slim_outputs.end(), nullptr);
ET_LOG(
Info,
"CUDA graph: cannot resize output %zu of '%s' to its captured "
"shape",
i,
handle->method_name.c_str());
return resize_err;
}
}

gerr = cudaGraphInstantiate(
Expand All @@ -1101,7 +1173,6 @@ class ET_EXPERIMENTAL CudaBackend final
Internal,
"cudaGraphInstantiate failed: %s",
cudaGetErrorString(gerr));
handle->cuda_graph_state.note_graph_allocations();

// Record static output pointers (stable under graph replay). Releasing
// them from slim_outputs here, before the copies below, keeps the cleanup
Expand All @@ -1127,6 +1198,8 @@ class ET_EXPERIMENTAL CudaBackend final
Internal,
"cudaGraphLaunch (first replay) failed: %s",
cudaGetErrorString(gerr));
graph_ran = true;
handle->cuda_graph_state.note_graph_allocations();

// Copy capture-step outputs into the planned GPU ET buffers (D2D).
for (size_t i = 0; i < n_outputs; i++) {
Expand All @@ -1143,7 +1216,7 @@ class ET_EXPERIMENTAL CudaBackend final
// Last failure point is behind us, so the captured state is now the state
// the next call should replay from.
capture_guard.disarm();
return commit_offgraph_kv_step(handle, kv_step_width);
return Error::Ok;
}

// ----- Normal / WARMUP execution continues here -----
Expand Down Expand Up @@ -1200,9 +1273,10 @@ class ET_EXPERIMENTAL CudaBackend final
slim_outputs[i] = nullptr;
}

return commit_offgraph_kv_step(handle, kv_step_width);
return Error::Ok;
}

public:
void destroy(DelegateHandle* handle_) const override {
if (handle_ == nullptr) {
return;
Expand Down
Loading
Loading