Skip to content

Commit ee71fb4

Browse files
committed
[executorch][PR] add cuda backend to backend test infra
Pull Request resolved: #17873 Integrate cuda backend into backend test infra; skipped the unsupported test for now ghstack-source-id: 348067577 @exported-using-ghexport Differential Revision: [D93019490](https://our.internmc.facebook.com/intern/diff/D93019490/)
1 parent 01d21fa commit ee71fb4

8 files changed

Lines changed: 267 additions & 92 deletions

File tree

.ci/scripts/test_backend.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ if [[ "$FLOW" == *vulkan* ]]; then
5656
EXTRA_BUILD_ARGS+=" -DEXECUTORCH_BUILD_VULKAN=ON"
5757
fi
5858

59+
if [[ "$FLOW" == *cuda* ]]; then
60+
# Fix libstdc++ GLIBCXX version for CUDA backend.
61+
# The embedded .so files in the CUDA blob require GLIBCXX_3.4.30
62+
# which the default conda libstdc++ doesn't have.
63+
echo "Installing newer libstdc++ for CUDA backend..."
64+
conda install -y -c conda-forge 'libstdcxx-ng>=12'
65+
export LD_LIBRARY_PATH="${CONDA_PREFIX}/lib:${LD_LIBRARY_PATH:-}"
66+
fi
67+
5968
if [[ "$FLOW" == *arm* ]]; then
6069

6170
# Setup ARM deps.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Test CUDA Backend
2+
3+
on:
4+
schedule:
5+
- cron: 0 2 * * *
6+
push:
7+
branches:
8+
- release/*
9+
tags:
10+
- ciflow/nightly/*
11+
pull_request:
12+
paths:
13+
- .github/workflows/test-backend-cuda.yml
14+
- .github/workflows/_test_backend.yml
15+
workflow_dispatch:
16+
17+
concurrency:
18+
group: ${{ github.workflow }}--${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
test-cuda:
23+
uses: ./.github/workflows/_test_backend.yml
24+
with:
25+
backend: cuda
26+
flows: '["cuda"]'
27+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
28+
timeout: 120
29+
run-linux: true
30+
runner-linux: linux.g5.4xlarge.nvidia.gpu

backends/cuda/test/tester.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
from typing import Any, List, Optional, Tuple
8+
9+
import executorch
10+
import executorch.backends.test.harness.stages as BaseStages
11+
import torch
12+
from executorch.backends.cuda.cuda_backend import CudaBackend
13+
from executorch.backends.cuda.cuda_partitioner import CudaPartitioner
14+
from executorch.backends.test.harness import Tester as TesterBase
15+
from executorch.backends.test.harness.stages import StageType
16+
from executorch.exir import EdgeCompileConfig
17+
from executorch.exir.backend.partitioner import Partitioner
18+
19+
20+
def _create_default_partitioner() -> CudaPartitioner:
21+
"""Create a CudaPartitioner with default compile specs."""
22+
compile_specs = [CudaBackend.generate_method_name_compile_spec("forward")]
23+
return CudaPartitioner(compile_specs)
24+
25+
26+
class ToEdgeTransformAndLower(BaseStages.ToEdgeTransformAndLower):
27+
"""CUDA-specific ToEdgeTransformAndLower stage."""
28+
29+
def __init__(
30+
self,
31+
partitioners: Optional[List[Partitioner]] = None,
32+
edge_compile_config: Optional[EdgeCompileConfig] = None,
33+
):
34+
if partitioners is None:
35+
partitioners = [_create_default_partitioner()]
36+
37+
super().__init__(
38+
default_partitioner_cls=_create_default_partitioner,
39+
partitioners=partitioners,
40+
edge_compile_config=edge_compile_config
41+
or EdgeCompileConfig(_check_ir_validity=False),
42+
)
43+
44+
45+
class CudaTester(TesterBase):
46+
"""
47+
Tester subclass for CUDA backend.
48+
49+
This tester defines the recipe for lowering models to the CUDA backend
50+
using AOTInductor compilation.
51+
"""
52+
53+
def __init__(
54+
self,
55+
module: torch.nn.Module,
56+
example_inputs: Tuple[torch.Tensor],
57+
dynamic_shapes: Optional[Tuple[Any]] = None,
58+
):
59+
stage_classes = (
60+
executorch.backends.test.harness.Tester.default_stage_classes()
61+
| {
62+
StageType.TO_EDGE_TRANSFORM_AND_LOWER: ToEdgeTransformAndLower,
63+
}
64+
)
65+
66+
super().__init__(
67+
module=module,
68+
stage_classes=stage_classes,
69+
example_inputs=example_inputs,
70+
dynamic_shapes=dynamic_shapes,
71+
)

backends/test/harness/stages/serialize.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import copy
22
import logging
3-
4-
from typing import Optional
3+
from typing import Dict, Optional
54

65
from executorch.backends.test.harness.stages.stage import Stage, StageType
76
from executorch.exir import ExecutorchProgramManager
8-
97
from torch.utils._pytree import tree_flatten
108

119
logger = logging.getLogger(__name__)
@@ -23,12 +21,15 @@
2321
class Serialize(Stage):
2422
def __init__(self):
2523
self.buffer = None
24+
self.data_files: Dict[str, bytes] = {}
2625

2726
def stage_type(self) -> StageType:
2827
return StageType.SERIALIZE
2928

3029
def run(self, artifact: ExecutorchProgramManager, inputs=None) -> None:
3130
self.buffer = artifact.buffer
31+
# Capture external data files (e.g., .ptd files for CUDA backend)
32+
self.data_files = artifact.data_files
3233

3334
@property
3435
def artifact(self) -> bytes:
@@ -40,8 +41,29 @@ def graph_module(self) -> None:
4041

4142
def run_artifact(self, inputs):
4243
inputs_flattened, _ = tree_flatten(inputs)
44+
45+
# Combine all external data files into a single buffer for data_map_buffer
46+
# Most backends have at most one external data file, but we concatenate
47+
# in case there are multiple (though this may not be fully supported)
48+
data_map_buffer = None
49+
if self.data_files:
50+
# If there's exactly one data file, use it directly
51+
# Otherwise, log a warning - multiple external files may need special handling
52+
if len(self.data_files) == 1:
53+
data_map_buffer = list(self.data_files.values())[0]
54+
else:
55+
# For multiple files, we use the first one and warn
56+
# This is a limitation - proper handling would need runtime support
57+
logger.warning(
58+
f"Multiple external data files found ({list(self.data_files.keys())}). "
59+
f"Using the first one. This may not work correctly for all backends."
60+
)
61+
data_map_buffer = list(self.data_files.values())[0]
62+
4363
executorch_module = _load_for_executorch_from_buffer(
44-
self.buffer, program_verification=Verification.Minimal
64+
self.buffer,
65+
data_map_buffer=data_map_buffer,
66+
program_verification=Verification.Minimal,
4567
)
4668
executorch_output = copy.deepcopy(
4769
executorch_module.run_method("forward", tuple(inputs_flattened))

backends/test/suite/conftest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import pytest
55
import torch
6-
76
from executorch.backends.test.suite.flow import all_flows
87
from executorch.backends.test.suite.reporting import _sum_op_counts
98
from executorch.backends.test.suite.runner import run_test
@@ -88,7 +87,14 @@ def lower_and_run_model(
8887
ids=str,
8988
)
9089
def test_runner(request):
91-
return TestRunner(request.param, request.node.name, request.node.originalname)
90+
flow = request.param
91+
test_name = request.node.name
92+
93+
# Check if this test should be skipped based on the flow's skip_patterns
94+
if flow.should_skip_test(test_name):
95+
pytest.skip(f"Test '{test_name}' matches skip pattern for flow '{flow.name}'")
96+
97+
return TestRunner(flow, test_name, request.node.originalname)
9298

9399

94100
@pytest.hookimpl(optionalhook=True)

backends/test/suite/flow.py

Lines changed: 85 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# LICENSE file in the root directory of this source tree.
55

66
import logging
7-
87
from dataclasses import dataclass, field
98
from typing import Callable
109

@@ -53,98 +52,98 @@ def __str__(self):
5352
return self.name
5453

5554

56-
def all_flows() -> dict[str, TestFlow]:
57-
flows = []
58-
59-
from executorch.backends.test.suite.flows.portable import PORTABLE_TEST_FLOW
55+
def _try_import_flows(
56+
module_path: str, flow_names: list[str], backend_name: str
57+
) -> list[TestFlow]:
58+
"""
59+
Attempt to import test flows from a module.
6060
61-
flows += [
62-
PORTABLE_TEST_FLOW,
63-
]
61+
Args:
62+
module_path: The full module path to import from.
63+
flow_names: List of flow variable names to import from the module.
64+
backend_name: Human-readable name for logging on failure.
6465
66+
Returns:
67+
List of imported TestFlow objects, or empty list if import fails.
68+
"""
6569
try:
66-
from executorch.backends.test.suite.flows.xnnpack import (
67-
XNNPACK_DYNAMIC_INT8_PER_CHANNEL_TEST_FLOW,
68-
XNNPACK_STATIC_INT8_PER_CHANNEL_TEST_FLOW,
69-
XNNPACK_STATIC_INT8_PER_TENSOR_TEST_FLOW,
70-
XNNPACK_TEST_FLOW,
71-
)
72-
73-
flows += [
74-
XNNPACK_TEST_FLOW,
75-
XNNPACK_DYNAMIC_INT8_PER_CHANNEL_TEST_FLOW,
76-
XNNPACK_STATIC_INT8_PER_CHANNEL_TEST_FLOW,
77-
XNNPACK_STATIC_INT8_PER_TENSOR_TEST_FLOW,
78-
]
79-
except Exception as e:
80-
logger.info(f"Skipping XNNPACK flow registration: {e}")
70+
import importlib
8171

82-
try:
83-
from executorch.backends.test.suite.flows.coreml import (
84-
COREML_STATIC_INT8_TEST_FLOW,
85-
COREML_TEST_FLOW,
86-
)
87-
88-
flows += [
89-
COREML_TEST_FLOW,
90-
COREML_STATIC_INT8_TEST_FLOW,
91-
]
72+
module = importlib.import_module(module_path)
73+
return [getattr(module, name) for name in flow_names]
9274
except Exception as e:
93-
logger.info(f"Skipping Core ML flow registration: {e}")
75+
logger.info(f"Skipping {backend_name} flow registration: {e}")
76+
return []
77+
78+
79+
# Registry of backend flows to import: (module_path, flow_names, backend_name)
80+
_FLOW_REGISTRY: list[tuple[str, list[str], str]] = [
81+
(
82+
"executorch.backends.test.suite.flows.xnnpack",
83+
[
84+
"XNNPACK_TEST_FLOW",
85+
"XNNPACK_DYNAMIC_INT8_PER_CHANNEL_TEST_FLOW",
86+
"XNNPACK_STATIC_INT8_PER_CHANNEL_TEST_FLOW",
87+
"XNNPACK_STATIC_INT8_PER_TENSOR_TEST_FLOW",
88+
],
89+
"XNNPACK",
90+
),
91+
(
92+
"executorch.backends.test.suite.flows.coreml",
93+
[
94+
"COREML_TEST_FLOW",
95+
"COREML_STATIC_INT8_TEST_FLOW",
96+
],
97+
"Core ML",
98+
),
99+
(
100+
"executorch.backends.test.suite.flows.vulkan",
101+
[
102+
"VULKAN_TEST_FLOW",
103+
"VULKAN_STATIC_INT8_PER_CHANNEL_TEST_FLOW",
104+
],
105+
"Vulkan",
106+
),
107+
(
108+
"executorch.backends.test.suite.flows.qualcomm",
109+
[
110+
"QNN_TEST_FLOW",
111+
"QNN_16A16W_TEST_FLOW",
112+
"QNN_16A8W_TEST_FLOW",
113+
"QNN_16A4W_TEST_FLOW",
114+
"QNN_16A4W_BLOCK_TEST_FLOW",
115+
"QNN_8A8W_TEST_FLOW",
116+
],
117+
"QNN",
118+
),
119+
(
120+
"executorch.backends.test.suite.flows.arm",
121+
[
122+
"ARM_TOSA_FP_FLOW",
123+
"ARM_TOSA_INT_FLOW",
124+
"ARM_ETHOS_U55_FLOW",
125+
"ARM_ETHOS_U85_FLOW",
126+
"ARM_VGF_FP_FLOW",
127+
"ARM_VGF_INT_FLOW",
128+
],
129+
"ARM",
130+
),
131+
(
132+
"executorch.backends.test.suite.flows.cuda",
133+
[
134+
"CUDA_TEST_FLOW",
135+
],
136+
"CUDA",
137+
),
138+
]
94139

95-
try:
96-
from executorch.backends.test.suite.flows.vulkan import (
97-
VULKAN_STATIC_INT8_PER_CHANNEL_TEST_FLOW,
98-
VULKAN_TEST_FLOW,
99-
)
100-
101-
flows += [
102-
VULKAN_TEST_FLOW,
103-
VULKAN_STATIC_INT8_PER_CHANNEL_TEST_FLOW,
104-
]
105-
except Exception as e:
106-
logger.info(f"Skipping Vulkan flow registration: {e}")
107140

108-
try:
109-
from executorch.backends.test.suite.flows.qualcomm import (
110-
QNN_16A16W_TEST_FLOW,
111-
QNN_16A4W_BLOCK_TEST_FLOW,
112-
QNN_16A4W_TEST_FLOW,
113-
QNN_16A8W_TEST_FLOW,
114-
QNN_8A8W_TEST_FLOW,
115-
QNN_TEST_FLOW,
116-
)
117-
118-
flows += [
119-
QNN_TEST_FLOW,
120-
QNN_16A16W_TEST_FLOW,
121-
QNN_16A8W_TEST_FLOW,
122-
QNN_16A4W_TEST_FLOW,
123-
QNN_16A4W_BLOCK_TEST_FLOW,
124-
QNN_8A8W_TEST_FLOW,
125-
]
126-
except Exception as e:
127-
logger.info(f"Skipping QNN flow registration: {e}")
141+
def all_flows() -> dict[str, TestFlow]:
142+
from executorch.backends.test.suite.flows.portable import PORTABLE_TEST_FLOW
128143

129-
try:
130-
from executorch.backends.test.suite.flows.arm import (
131-
ARM_ETHOS_U55_FLOW,
132-
ARM_ETHOS_U85_FLOW,
133-
ARM_TOSA_FP_FLOW,
134-
ARM_TOSA_INT_FLOW,
135-
ARM_VGF_FP_FLOW,
136-
ARM_VGF_INT_FLOW,
137-
)
138-
139-
flows += [
140-
ARM_TOSA_FP_FLOW,
141-
ARM_TOSA_INT_FLOW,
142-
ARM_ETHOS_U55_FLOW,
143-
ARM_ETHOS_U85_FLOW,
144-
ARM_VGF_FP_FLOW,
145-
ARM_VGF_INT_FLOW,
146-
]
147-
except Exception as e:
148-
logger.info(f"Skipping ARM flow registration: {e}")
144+
flows = [PORTABLE_TEST_FLOW]
145+
146+
for module_path, flow_names, backend_name in _FLOW_REGISTRY:
147+
flows.extend(_try_import_flows(module_path, flow_names, backend_name))
149148

150149
return {f.name: f for f in flows if f is not None}

0 commit comments

Comments
 (0)