executorch: fall back to the CUDA backend for ops TensorRT can't take#4404
executorch: fall back to the CUDA backend for ops TensorRT can't take#4404Conarnar wants to merge 2 commits into
Conversation
When saving with output_format="executorch", ops that TensorRT does not convert (unsupported ops, or ops pinned via torch_executed_ops) were left on eager CPU kernels. Append an ExecuTorch CudaPartitioner as a catch-all after the TensorRTPartitioner so those ops run on ExecuTorch's CUDA (AOTInductor) backend instead. The fallback degrades gracefully: torch-tensorrt does not hard-depend on ExecuTorch's CUDA backend, so if executorch.backends.cuda is unavailable it logs a warning and leaves those ops on eager CPU kernels rather than failing the export. Add an end-to-end test (erfinv, which has no TRT converter) asserting the op lands on the CUDA backend delegate. Signed-off-by: Conan Jeffrey Truong <cjtruong@meta.com>
| "(AOTInductor) backend." | ||
| ) | ||
| else: | ||
| cuda_specs = [CudaBackend.generate_method_name_compile_spec("forward")] |
There was a problem hiding this comment.
can we dynamic choose the method name, instead of statically use forward?
Also in the test lets add a multi-method one.
There was a problem hiding this comment.
At the moment save only supports a single exported program which to_edge_transform_and_lower will default to the method name forward.
| ) | ||
|
|
||
| out = tmp_path / "erfinv_cuda_fallback.pte" | ||
| torch_tensorrt.save( |
There was a problem hiding this comment.
is the model runnable now? Can we handle the data transfer between different delegate blobs?
shoumikhin
left a comment
There was a problem hiding this comment.
A few points from a closer look, most important first.
-
Dropped external CUDA weights.
_save_as_executorchonly callsexecutorch_program.write_to_file(f), but the CUDA backend emits its weights as external named data (save_data_externallyis True, taggedaoti_<device>_blob) that ExecuTorch serializes only throughwrite_tensor_data_to_file. Since that is never called, any residual partition that carries weights loses its blob and the.ptecannot load. The new test does not catch this becausecos(erfinv(tanh(x)))has no parameters, so please persist the external data next to the.pteand add a test model with weights in the fallback partition. -
The import guard is too narrow. The
try/exceptonly catchesImportErroron the module import, but the real CUDA compile runs later insideto_edge_transform_and_lowerviaCudaBackend.preprocesscallingtorch._inductor.aot_compilewithmax_autotuneand the Triton backends, which needsnvcc/ptxas. On a host that has the executorch cuda wheel but no cuda toolkit, export now throws where before the residual op stayed non-delegated andsaveproduced a working CPU.pte, so the docstring line about degrading gracefully only holds for the missing-import case. Consider gating the fallback onptxasavailability or making it opt-in, and narrowing the docstring. -
Always on with no opt-out. Appending
CudaPartitionerunconditionally means any model with even one non-TRT op now embeds aCudaBackendblob and requires a CUDA runtime at load, where before those ops stayed non-delegated and the.pteran on CPU. Most real models are not fully TRT-convertible, and this also silently turns onmax_autotuneautotuning at save time, so please add asave()kwarg to keep the portable CPU-fallback behavior. -
target_deviceis not propagated to the fallback. It is forwarded only toTensorRTPartitioner; the fallback is built ascuda_specs = [CudaBackend.generate_method_name_compile_spec("forward")]with notarget_device, soCudaPartitionerdefaults tocuda:0. If a user pins TRT tocuda:1the two delegates end up on different devices, so derive the fallbacktarget_devicefrom the samecompile_specs. -
The test proves composition, not runnability. It only deserializes the program and checks that
CudaBackendandTensorRTBackendappear in the delegate ids; it never builds aMethod, runs, or compares to eager. The mixed ATen-mode roundtrip is also still gated on thetensor_parser_atenfix (planned CUDA buffers get a CPU data ptr soMethod::initfails ingetDeviceFromPtr), so scope the assertion to "produces both delegates" and add a real load-run-allclose once that lands.
|
Always enjoy reading Anthony's comment
|
- Make cuda_fallback opt-in (default False) so a partially-convertible model still produces a portable, CPU-loadable .pte and does not pull in a CUDA runtime or run AOTInductor autotuning at save time unless requested. - Persist the CUDA backend's external named data next to the .pte via write_tensor_data_to_file; a fallback partition carrying weights would otherwise lose its blob and fail to load. - Propagate the caller's target_device compile spec to the CudaPartitioner so the fallback lands on the same GPU as the TRT engines (was hardcoded cuda:0). - Fix _exporter.py: lifted parameter/buffer placeholders now keep their real device and dtype (torch.empty_strided defaulted them to cpu/float32), which caused FakeTensorDeviceMismatchError when a weighted op was excluded from TRT. - Narrow the docstring and raise a clear error when cuda_fallback is requested but executorch.backends.cuda is unavailable. - Tests: exercise cuda_fallback=True, scope assertions to delegate composition (load-run gated on the tensor_parser_aten fix), and add a weighted-fallback test guarding external-data persistence. Signed-off-by: Conan Jeffrey Truong <cjtruong@meta.com>
Description
When saving with
output_format="executorch", ops that TensorRT does not convert (unsupported ops, or ops pinned viatorch_executed_ops) are left on eager CPU kernels. This adds an opt-in CUDA fallback:save(..., cuda_fallback=True)appends an ExecuTorchCudaPartitioneras a catch-all after theTensorRTPartitioner, so those ops run on ExecuTorch's CUDA (AOTInductor) backend instead.Opt-in (
cuda_fallback=Falseby default). Enabling it requires ExecuTorch's CUDA backend and a CUDA toolkit (nvcc/ptxas) at export time, runs AOTInductor autotuning, and produces a.ptethat needs a CUDA runtime at load. Leaving it off keeps the previous behavior — TRT-excluded ops stay non-delegated and the.pteloads on CPU — so partially-convertible models are unaffected. Ifcuda_fallback=Trueis requested butexecutorch.backends.cudais unavailable,save()raises a clear error rather than silently degrading.External weights. The CUDA backend emits its weights as external named data;
save()now persists them (write_tensor_data_to_file) next to the.pte, so a fallback partition that carries weights doesn't lose its blob and fail to load.Device consistency.
target_deviceis derived from the caller'scompile_specs, so the CUDA partition lands on the same GPU as the TRT engines (previously hardcodedcuda:0)._exporter.py: lifted parameter/buffer placeholders now keep their real device and dtype. Previouslytorch.empty_strided(...)defaulted them tocpu/float32, which caused aFakeTensorDeviceMismatchError(cuda:0 vs cpu) when a weighted op was excluded from TensorRT.Testing
Added
tests/py/dynamo/executorch/test_cuda_fallback.py:test_erfinv_falls_back_to_cuda_backend—erfinv(no TRT converter, notorch_executed_opspin needed) lands on the CUDA backend whiletanh/cosstay on TensorRT.test_weighted_fallback_persists_external_data— a weighted, TRT-excludedmmproduces a CUDA delegate and its external.ptddata is written next to the.pte.Both are scoped to delegate composition + external-data persistence; a load-run-allclose check is deferred until the mixed ATen-mode runtime path (
tensor_parser_aten) lands. Tests skip cleanly without a GPU / the CUDA backend / nvcc.