Skip to content

Commit eadb4c3

Browse files
authored
[cuda backend] support multi-SM AOTI PTEs (pytorch#22198)
### Summary Add one current `ETCUDAFQN0` metadata format that stores target-specific CUDA AOTI shared libraries with one shared FQN weight manifest. At runtime, CUDA first selects an exact-SM regular variant. If no exact regular variant exists, it may use the PTX from one explicitly designated fallback PTE when that PTX is compatible with the current GPU. The fallback is no longer inferred from the lowest regular target SM. Add a PTE/PTD merge tool with two input classes: - One or more regular PTEs provide exact-SM native variants. Their merged metadata sets `ptx_compute=0`, so they are never considered as PTX fallbacks. Regular exports should use `CompileSpec("cuda_include_ptx", b"OFF")` to avoid embedding redundant PTX. - At most one fallback PTE provides the PTX-capable variant. It is marked `fallback_only`, is never selected as an exact native variant, and is considered only when the merged PTE has no regular variant for the current SM. The merge validates the ExecuTorch program, CUDA delegate layout, non-CUDA named data, weight metadata, and streamed PTD weight hashes. CUDA PTE merging is NVIDIA-only and explicitly rejects ROCm. ### Merged artifact layout For an A100 (`sm80`) regular export, an RTX 5090 (`sm120`) regular export, and one PTX fallback export, the merged artifact is structured as follows: ```text merged.pte ├── program and CUDA delegates │ └── `ETCUDAFQN0` CUDA variant metadata (per delegate) │ ├── regular: target_sm=80, ptx_compute=0 -> sm80 source SO key │ ├── regular: target_sm=120, ptx_compute=0 -> sm120 source SO key │ └── fallback_only: target_sm=<source SM>, │ ptx_compute=<fallback compute capability> -> fallback source SO key ├── named data │ ├── sm80 source SO key -> actual AOTI SO containing the prebuilt sm80 cubin │ ├── sm120 source SO key -> actual AOTI SO containing the prebuilt sm120 cubin │ └── fallback source SO key -> actual PTX-bearing fallback AOTI SO └── one shared FQN weight manifest merged.ptd └── shared external tensor data reused from the regular base export ``` The tool copies each AOTI SO atomically rather than rewriting its ELF/fatbin. The regular/fallback distinction is encoded in the merged metadata: only regular variants participate in exact-SM native selection, while the fallback SO contributes PTX only to fallback selection. After merging, the CLI prints a provenance table mapping every stored SM cubin and the fallback PTX entry to its source PTE. This PR was authored with OpenAI Codex. ### Test plan The primary validation is a real cross-hardware CUDA CI pipeline spanning A100 (`sm80`) and A10G (`sm86`): 1. Export an A100 native PTE and an A10G native PTE independently on their respective GPUs, both with PTX disabled and isolated Inductor caches. 2. Export a separate PTX fallback PTE on A100 with the A10G shared-memory constraint. 3. Verify byte-for-byte that the A100 native PTD, A10G native PTD, fallback PTD, and final merged PTD are identical. 4. On A10G, verify the A10G native PTE succeeds, the A100 native-only PTE is rejected with no compatible native/PTX variant, and the merged PTE succeeds. 5. On A100, verify the A100 native PTE succeeds, the A10G native-only PTE is rejected with no compatible native/PTX variant, and the same merged PTE succeeds. Additional Python coverage exercises the single `ETCUDAFQN0` metadata format, untargeted ROCm metadata, explicit fallback-only encoding, PTE/PTD merging, shared-weight validation, duplicate regular SM rejection, provenance reporting, and ROCm merge rejection. C++ coverage exercises metadata parsing, exact regular selection, PTX fallback selection, incompatibility, and duplicate targets. Locally ran the 17 targeted CUDA metadata/merge tests, Python lint/format checks, workflow YAML parsing, and `git diff --check`.
1 parent 75c2b30 commit eadb4c3

15 files changed

Lines changed: 1985 additions & 35 deletions

‎.github/workflows/cuda.yml‎

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ jobs:
160160
done
161161
162162
export-cuda-target-smem-cross-arch:
163-
name: export-cuda-target-smem-cross-arch-a100
163+
name: export-cuda-multi-arch-a100
164164
needs: [changed-files, run-decision]
165165
if: |
166166
contains(needs.changed-files.outputs.changed-files, 'backends/cuda') ||
@@ -178,15 +178,13 @@ jobs:
178178
gpu-arch-version: "13.0"
179179
use-custom-docker-registry: false
180180
submodules: recursive
181-
upload-artifact: cuda-target-smem-cross-arch
181+
upload-artifact: cuda-multi-arch-a100
182182
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
183183
script: |
184184
set -eux
185185
186186
PYTHON_EXECUTABLE=python ./install_executorch.sh
187187
export LD_LIBRARY_PATH="/opt/conda/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
188-
# A10G (sm_86) supports 99 KiB of opt-in shared memory per block.
189-
export ET_CUDA_TARGET_SMEM_BYTES=101376
190188
# The A100 exporter has an AVX-512 host CPU while the A10G runner has
191189
# AVX2. Keep the generated AOTI host wrapper compatible with both.
192190
export ATEN_CPU_CAPABILITY=avx2
@@ -199,16 +197,33 @@ jobs:
199197
assert capability == (8, 0), f"Expected A100 sm_80 exporter, got {capability}"
200198
PY
201199
200+
# Export an sm80 native PTE with no PTX. This artifact must fail on the
201+
# A10G because its metadata has neither an sm86 native variant nor PTX.
202+
model_dir="${RUNNER_ARTIFACT_DIR}/native-sm80/linear"
203+
mkdir -p "${model_dir}"
204+
TORCHINDUCTOR_CACHE_DIR="${RUNNER_TEMP}/inductor-native-sm80" \
205+
python -m examples.cuda.scripts.export \
206+
--model_name=linear \
207+
--output_dir="${model_dir}" \
208+
--cuda_include_ptx=OFF \
209+
--seed=0
210+
211+
# Export the explicit PTX fallback separately. A10G supports 99 KiB of
212+
# opt-in shared memory per block, so constrain the A100 export to it.
202213
for model in linear sdpa; do
203-
model_dir="${RUNNER_ARTIFACT_DIR}/${model}"
214+
model_dir="${RUNNER_ARTIFACT_DIR}/fallback-sm80/${model}"
204215
mkdir -p "${model_dir}"
205-
python -m examples.cuda.scripts.export \
216+
ET_CUDA_TARGET_SMEM_BYTES=101376 \
217+
TORCHINDUCTOR_CACHE_DIR="${RUNNER_TEMP}/inductor-fallback-sm80-${model}" \
218+
python -m examples.cuda.scripts.export \
206219
--model_name="${model}" \
207-
--output_dir "${model_dir}"
220+
--output_dir="${model_dir}" \
221+
--cuda_include_ptx=ON \
222+
--seed=0
208223
done
209224
210225
test-cuda-target-smem-cross-arch:
211-
name: test-cuda-target-smem-cross-arch-a10g
226+
name: merge-and-test-cuda-multi-arch-a10g
212227
needs: [export-cuda-target-smem-cross-arch]
213228
if: needs.export-cuda-target-smem-cross-arch.result == 'success'
214229
uses: pytorch/test-infra/.github/workflows/linux_job_v3.yml@main
@@ -222,7 +237,8 @@ jobs:
222237
gpu-arch-version: "13.0"
223238
use-custom-docker-registry: false
224239
submodules: recursive
225-
download-artifact: cuda-target-smem-cross-arch
240+
download-artifact: cuda-multi-arch-a100
241+
upload-artifact: cuda-multi-arch-merged
226242
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
227243
script: |
228244
set -eux
@@ -243,20 +259,141 @@ jobs:
243259
)
244260
PY
245261
262+
# Independently export an sm86 native PTE with no PTX. A fixed seed
263+
# makes its external weights byte-identical to both A100 exports.
264+
model_dir="${RUNNER_ARTIFACT_DIR}/native-sm86/linear"
265+
mkdir -p "${model_dir}"
266+
TORCHINDUCTOR_CACHE_DIR="${RUNNER_TEMP}/inductor-native-sm86" \
267+
python -m examples.cuda.scripts.export \
268+
--model_name=linear \
269+
--output_dir="${model_dir}" \
270+
--cuda_include_ptx=OFF \
271+
--seed=0
272+
273+
sm80_ptd="${RUNNER_ARTIFACT_DIR}/native-sm80/linear/aoti_cuda_blob.ptd"
274+
sm86_ptd="${RUNNER_ARTIFACT_DIR}/native-sm86/linear/aoti_cuda_blob.ptd"
275+
fallback_ptd="${RUNNER_ARTIFACT_DIR}/fallback-sm80/linear/aoti_cuda_blob.ptd"
276+
cmp "${sm80_ptd}" "${sm86_ptd}"
277+
cmp "${sm80_ptd}" "${fallback_ptd}"
278+
sha256sum "${sm80_ptd}" "${sm86_ptd}" "${fallback_ptd}"
279+
280+
merged_dir="${RUNNER_ARTIFACT_DIR}/merged"
281+
mkdir -p "${merged_dir}"
282+
python -m executorch.backends.cuda.merge_ptes \
283+
--input-pte "${RUNNER_ARTIFACT_DIR}/native-sm80/linear/linear.pte" \
284+
--input-pte "${RUNNER_ARTIFACT_DIR}/native-sm86/linear/linear.pte" \
285+
--input-ptd "${sm80_ptd}" \
286+
--input-ptd "${sm86_ptd}" \
287+
--fallback-pte "${RUNNER_ARTIFACT_DIR}/fallback-sm80/linear/linear.pte" \
288+
--fallback-ptd "${fallback_ptd}" \
289+
--output-pte "${merged_dir}/linear.pte" \
290+
--output-ptd "${merged_dir}/aoti_cuda_blob.ptd" \
291+
| tee "${merged_dir}/provenance.txt"
292+
cmp "${sm80_ptd}" "${merged_dir}/aoti_cuda_blob.ptd"
293+
246294
cmake -DCMAKE_BUILD_TYPE=Release \
247295
-DEXECUTORCH_BUILD_CUDA=ON \
248296
-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \
297+
-DEXECUTORCH_ENABLE_LOGGING=ON \
249298
-DPYTHON_EXECUTABLE=python \
250299
-Bcmake-out .
251300
cmake --build cmake-out --target executor_runner -j4
252301
253302
for model in linear sdpa; do
254-
model_dir="${RUNNER_ARTIFACT_DIR}/${model}"
303+
model_dir="${RUNNER_ARTIFACT_DIR}/fallback-sm80/${model}"
255304
./cmake-out/executor_runner \
256305
--model_path "${model_dir}/${model}.pte" \
257306
--data_path "${model_dir}/aoti_cuda_blob.ptd"
258307
done
259308
309+
# The local native PTE and merged PTE must run on sm86.
310+
./cmake-out/executor_runner \
311+
--model_path "${RUNNER_ARTIFACT_DIR}/native-sm86/linear/linear.pte" \
312+
--data_path "${sm86_ptd}"
313+
./cmake-out/executor_runner \
314+
--model_path "${merged_dir}/linear.pte" \
315+
--data_path "${merged_dir}/aoti_cuda_blob.ptd"
316+
317+
# The sm80 native-only PTE must be rejected on sm86.
318+
set +e
319+
./cmake-out/executor_runner \
320+
--model_path "${RUNNER_ARTIFACT_DIR}/native-sm80/linear/linear.pte" \
321+
--data_path "${sm80_ptd}" \
322+
>"${RUNNER_ARTIFACT_DIR}/native-sm80-on-sm86.log" 2>&1
323+
native_status=$?
324+
set -e
325+
cat "${RUNNER_ARTIFACT_DIR}/native-sm80-on-sm86.log"
326+
test "${native_status}" -ne 0
327+
grep -q "no native or PTX variant compatible with sm86" \
328+
"${RUNNER_ARTIFACT_DIR}/native-sm80-on-sm86.log"
329+
330+
test-cuda-merged-pte-a100:
331+
name: test-cuda-merged-pte-a100
332+
needs: [test-cuda-target-smem-cross-arch]
333+
if: needs.test-cuda-target-smem-cross-arch.result == 'success'
334+
uses: pytorch/test-infra/.github/workflows/linux_job_v3.yml@main
335+
permissions:
336+
id-token: write
337+
contents: read
338+
with:
339+
timeout: 90
340+
runner: mt-l-x86iavx512-11-125-a100
341+
gpu-arch-type: cuda
342+
gpu-arch-version: "13.0"
343+
use-custom-docker-registry: false
344+
submodules: recursive
345+
download-artifact: cuda-multi-arch-merged
346+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
347+
script: |
348+
set -eux
349+
350+
PYTHON_EXECUTABLE=python ./install_executorch.sh
351+
export LD_LIBRARY_PATH="/opt/conda/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
352+
353+
python - <<'PY'
354+
import torch
355+
356+
capability = torch.cuda.get_device_capability()
357+
assert capability == (8, 0), f"Expected A100 sm_80 runner, got {capability}"
358+
PY
359+
360+
cmake -DCMAKE_BUILD_TYPE=Release \
361+
-DEXECUTORCH_BUILD_CUDA=ON \
362+
-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \
363+
-DEXECUTORCH_ENABLE_LOGGING=ON \
364+
-DPYTHON_EXECUTABLE=python \
365+
-Bcmake-out .
366+
cmake --build cmake-out --target executor_runner -j4
367+
368+
sm80_ptd="${RUNNER_ARTIFACT_DIR}/native-sm80/linear/aoti_cuda_blob.ptd"
369+
sm86_ptd="${RUNNER_ARTIFACT_DIR}/native-sm86/linear/aoti_cuda_blob.ptd"
370+
fallback_ptd="${RUNNER_ARTIFACT_DIR}/fallback-sm80/linear/aoti_cuda_blob.ptd"
371+
merged_ptd="${RUNNER_ARTIFACT_DIR}/merged/aoti_cuda_blob.ptd"
372+
cmp "${sm80_ptd}" "${sm86_ptd}"
373+
cmp "${sm80_ptd}" "${fallback_ptd}"
374+
cmp "${sm80_ptd}" "${merged_ptd}"
375+
376+
# The local native PTE and merged PTE must run on sm80.
377+
./cmake-out/executor_runner \
378+
--model_path "${RUNNER_ARTIFACT_DIR}/native-sm80/linear/linear.pte" \
379+
--data_path "${sm80_ptd}"
380+
./cmake-out/executor_runner \
381+
--model_path "${RUNNER_ARTIFACT_DIR}/merged/linear.pte" \
382+
--data_path "${merged_ptd}"
383+
384+
# The sm86 native-only PTE must be rejected on sm80.
385+
set +e
386+
./cmake-out/executor_runner \
387+
--model_path "${RUNNER_ARTIFACT_DIR}/native-sm86/linear/linear.pte" \
388+
--data_path "${sm86_ptd}" \
389+
>"${RUNNER_ARTIFACT_DIR}/native-sm86-on-sm80.log" 2>&1
390+
native_status=$?
391+
set -e
392+
cat "${RUNNER_ARTIFACT_DIR}/native-sm86-on-sm80.log"
393+
test "${native_status}" -ne 0
394+
grep -q "no native or PTX variant compatible with sm80" \
395+
"${RUNNER_ARTIFACT_DIR}/native-sm86-on-sm80.log"
396+
260397
unittest-cuda:
261398
name: unittest-cuda
262399
needs: [changed-files, run-decision]
@@ -370,6 +507,11 @@ jobs:
370507
conda install -y -c conda-forge 'libstdcxx-ng>=12'
371508
export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH
372509
510+
python -m pytest \
511+
backends/cuda/tests/test_cuda_weight_metadata.py \
512+
backends/cuda/tests/test_merge_ptes.py \
513+
-v -o "addopts="
514+
373515
cmake --preset llm-release-cuda -DEXECUTORCH_BUILD_TESTS=ON
374516
cmake --build cmake-out --target test_cuda_allocator test_cuda_mutable_state test_cuda_weight_cache -j$(nproc)
375517
ctest --test-dir cmake-out -R test_cuda_allocator --output-on-failure -V

‎backends/cuda/BUCK‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ fbcode_target(
109109
],
110110
)
111111

112+
fbcode_target(
113+
_kind = runtime.python_library,
114+
name = "merge_ptes",
115+
srcs = [
116+
"merge_ptes.py",
117+
],
118+
visibility = ["PUBLIC"],
119+
deps = [
120+
":cuda_backend",
121+
"//caffe2:torch",
122+
"//executorch/exir/_serialize:lib",
123+
"//executorch/exir:schema",
124+
"//executorch/extension/flat_tensor/serialize:serialize",
125+
],
126+
)
127+
112128
fbcode_target(
113129
_kind = runtime.python_library,
114130
name = "cuda_partitioner",

‎backends/cuda/cuda_backend.py‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,14 @@ def _setup_cuda_environment_for_fatbin() -> bool:
519519
except Exception:
520520
return False
521521

522+
@classmethod
523+
def _should_include_ptx(cls, compile_specs: List[CompileSpec]) -> bool:
524+
include_ptx = True
525+
for spec in compile_specs:
526+
if spec.key == "cuda_include_ptx":
527+
include_ptx = _on_off_compile_spec_value(spec)
528+
return include_ptx and cls._setup_cuda_environment_for_fatbin()
529+
522530
@classmethod
523531
def save_data_externally(cls) -> bool:
524532
"""
@@ -539,7 +547,29 @@ def _preprocess_with_weight_collector(
539547
result = super().preprocess(edge_program, compile_specs)
540548
if capture.artifact is None:
541549
raise RuntimeError("CUDA AOTI did not return a structured Weights output")
542-
collector.add_preprocess_result(result, capture.artifact, cls.get_device_name())
550+
target_sm = None
551+
ptx_compute = 0
552+
if torch.version.hip is None:
553+
from torch._inductor.codegen.cuda.compile_utils import (
554+
_nvcc_arch_as_compile_option,
555+
)
556+
557+
compiled_arch = _nvcc_arch_as_compile_option()
558+
target_arch = compiled_arch.removesuffix("a")
559+
if not target_arch.isdigit():
560+
raise RuntimeError(f"Unsupported CUDA architecture: {compiled_arch}")
561+
target_sm = int(target_arch)
562+
# Architecture-accelerated PTX targets such as compute_90a and
563+
# compute_120a are not forward-compatible fallback images.
564+
if cls._should_include_ptx(compile_specs) and compiled_arch.isdigit():
565+
ptx_compute = int(compiled_arch)
566+
collector.add_preprocess_result(
567+
result,
568+
capture.artifact,
569+
cls.get_device_name(),
570+
target_sm=target_sm,
571+
ptx_compute=ptx_compute,
572+
)
543573
return result
544574

545575
@classmethod
@@ -762,7 +792,7 @@ def get_aoti_compile_options(
762792

763793
# Configure CUDA environment variables based on detected version
764794

765-
emit_multi_arch_kernel = CudaBackend._setup_cuda_environment_for_fatbin()
795+
emit_multi_arch_kernel = cls._should_include_ptx(compile_specs)
766796
# Base options for all platforms
767797

768798
options: Dict[str, typing.Any] = {

0 commit comments

Comments
 (0)