Repository navigation
fix(codegen): raise when the device refuses a kernel launch - #256
Conversation
Test planThe new test in
A/B on a single sm_90 GPU
The new test is the only one that changes outcome. The four failures on both sides are two tests that need a second GPU and two that cannot find a CUDA header in this machine's toolkit layout. These sm_90 results come from the first commit of this PR; CI covers the current version on sm_90. A/B on a single sm_89 GPU
The new test passes. Four The 15 tests that fail on both sides now report that error in place of a value mismatch. The bundled target is sm_90, so those launches never ran on this GPU. With a small kernel built for sm_89:
Each time is the median of 7 rounds of 5000 calls. Across all runs the rounds range from 2.32 to 2.97 µs. |
|
I would also like to add sm_89 support to TileFoundry. The plan is an sm_89 architecture document, a device document for the card we test on, and the existing MMA, ldmatrix and cp.async paths running on that card. The device schema calls global memory If this fits your plans for TileFoundry, I would be glad to hear what you think. |
zhen8838
left a comment
There was a problem hiding this comment.
Thanks. The fix is right: a refused launch no longer returns as though the kernel ran. The test is precise: 1 MiB of dynamic shared memory is refused on every device, so it hits exactly the new path.
Two inline comments: one on how the error crosses the C ABI, one on what the launch status can attribute.
| ctx.emit("if (cudaError_t status = cudaGetLastError(); status != cudaSuccess) {") | ||
| ctx.indent() | ||
| ctx.emit( | ||
| f'throw std::runtime_error(std::string("tilefoundry: launch of {kernel.name} failed: ") ' |
There was a problem hiding this comment.
This throws a C++ exception out of an extern "C" function. The host unit declares the launcher (src/tilefoundry/codegen/cpu/module.py:211-218, _declare_callee) and calls it (:207), and the host entry is what tvm-ffi registers (src/tilefoundry/codegen/cpu/templates/cpu_entry.cpp.j2:8). This works today only because every caller is C++ inside one library. A C or ctypes caller would terminate.
Returning cudaError_t to the host is not an option either: docs/spec/codegen.md says the host module must not reference CUDA types.
Suggested shape:
-
The launcher returns
const char*:nullptron success,cudaGetErrorString(status)otherwise (a static string, so no lifetime issue). -
The host emits:
if (const char* error = <launcher>(...)) { TVM_FFI_THROW(RuntimeError) << "tilefoundry: launch of <name> failed: " << error; }
TVM_FFI_THROWcomes with<tvm/ffi/function.h>, which the host preamble already includes. -
Update both launcher declarations (
cuda/module.py:46and:82),_declare_callee,_one_launch, and the codegen.md sentence.
The error then crosses tvm-ffi as an ffi error of kind RuntimeError, and no exception crosses the C ABI. Today the std::runtime_error reaches Python as RuntimeError only by fallback: tvm-ffi's safe-call wrapper re-kinds any std::exception as InternalError, and Python maps unknown kinds to RuntimeError.
On the name: the host does not know the __global__ name. That is kernel_symbol() in cuda/abi.py (<mangled>_kernel). The host has the launcher name (ctx.signature_of(callee).name, <mangled>_launch) and the callee's FunctionSymbol (ctx.codegen_context.symbols.by_function[id(callee)]), whose export_name is the readable dotted name. export_name reads better in an error. The two spec sentences would then say "naming the launched function" instead of "the kernel".
There was a problem hiding this comment.
Thanks, this is a cleaner boundary. The launcher now returns the CUDA error text, or null on success, and the host entry raises it with TVM_FFI_THROW(RuntimeError), so no exception crosses the C ABI.
Both launcher declarations, _declare_callee, _one_launch and the codegen.md sentence are updated, and the two codegen tests that check the declaration now expect const char*. The message names the launched function. On a single sm_89 GPU the full suite gives the same results as with the first commit.
| fault while the kernel runs is asynchronous and surfaces at the next | ||
| synchronizing CUDA call. |
There was a problem hiding this comment.
cudaGetLastError() right after the launch also returns a sticky asynchronous error left by an earlier kernel, and any stale error an earlier runtime call left on this thread. So "launch of X failed" can name a kernel that did not cause the fault.
Suggest saying so here, for example: "The status read after the launch can also carry an asynchronous error from earlier device work, so the named kernel is where the error was observed, not necessarily where it occurred."
And let the message hint at it, the way PyTorch does ("CUDA kernel errors might be asynchronously reported at some other API call ... consider passing CUDA_LAUNCH_BLOCKING=1"). For example: tilefoundry: launch of <name> failed: <error> (this can be an asynchronous error from an earlier kernel; set CUDA_LAUNCH_BLOCKING=1 to locate it).
|
Thanks for this fix, and for offering sm_89 support: it is welcome. Please send it as a separate PR from this one. It is mostly data and checks; the code paths it needs already exist.
The The contract for both documents is |
Why
When the device refuses a kernel launch, the call returns as if the kernel had run. The launcher issues
kernel<<<grid, block, smem, nullptr>>>(...)and never reads the launch status, so the outputs keep their old contents and no error reaches the caller.On a single sm_89 GPU, tests built for the bundled sm_90 target fail only on their values, and
compute-sanitizershowscudaLaunchKernelreturningcudaErrorNoKernelImageForDevice.What
The CUDA launcher reads
cudaGetLastError()after the launch and returns the CUDA error text when the device refused it, or null otherwise. The host entry checks that result and raises it withTVM_FFI_THROW(RuntimeError), naming the launched function. No exception crosses the C ABI, and Python receives aRuntimeError.A new test in
tests/ops/tir/test_launch.pyasks for 1 MiB of dynamic shared memory, which every CUDA device refuses, and expects that error. The two codegen tests that check the launcher declaration now expectconst char*.Contract
docs/spec/codegen.mdrequires the launcher to return the CUDA error text of a refused launch and the host entry to raise it.docs/spec/runtime.mdstates that a refused launch raisesRuntimeErrorfrom the call.Risk
Calls whose launch was refused now raise. On a single sm_89 GPU this turns four
tests/scaffoldcases from passed to failed, because their sm_90 kernels never ran there. On a single sm_90 GPU they pass with and without this change.Each launch adds one
cudaGetLastError()call and one check on the host. The comment below shows no difference in time per call beyond the spread between runs.