modules/cuda: completely revamp microarchitecture handling

Replace the per-CUDA-version architecture lists with a declarative
table of support windows (min/max CUDA version per micro-ISA). There's
a clean separation between version bounds and logic, and once a new
arch like Rubin comes out, it's a table entry instead of a change to
complex intertwined logic.

User-visible changes:
- support the specifiers introduced with CUDA 12.8/12.9:
  architecture-specific 'X.Ya' ('Hopper(A)', '12.0a', ...) and
  family-specific 'X.Yf' ('10.0f', ...), plus the Thor and newer
  Blackwell parts
- min_driver_version() table extended through CUDA 13.3
- '+PTX' is now rejected for architecture-specific ('a') targets,
  which have no forward compatibility
- warnings distinguish "CUDA too old for arch" from "arch dropped
  from CUDA"

Part-of: https://github.com/mesonbuild/meson/pull/16002
Closes: https://github.com/mesonbuild/meson/pull/16002
Signed-off-by: Sam James <sam@gentoo.org>
This commit is contained in:
David Seifert authored and Sam James committed 2026-08-11 16:53:14 +01:00
1 parent 66ab168e6f
commit 38d558e31c
5 files changed
+402 -222

No files matched your search

+4
View File
@@ -30,4 +30,8 @@ extend-ignore =
A003
per-file-ignores =
mesonbuild/mesonlib.py:F401,F403
# E131: continuation line unaligned for hanging indent
# E201: whitespace after '('
# (both due to deliberate column alignment in the CUDA arch table)
mesonbuild/modules/cuda.py:E131,E201
max-line-length = 120
+25 -1
View File
@@ -76,7 +76,7 @@ The supported architecture names and their corresponding compute capabilities
are:
| Name | Compute Capability |
|-------------------|--------------------|
|-------------------|----------------------------|
| `'Fermi'` | 2.0, 2.1(2.0) |
| `'Kepler'` | 3.0, 3.5 |
| `'Kepler+Tegra'` | 3.2 |
@@ -89,6 +89,30 @@ are:
| `'Xavier'` | 7.2 |
| `'Turing'` | 7.5 |
| `'Ampere'` | 8.0, 8.6 |
| `'Orin'` | 8.7 |
| `'Lovelace'` | 8.9 |
| `'Hopper'` | 9.0 |
| `'Hopper(A)'` | 9.0a |
| `'Thor'` | 10.1, 11.0 |
| `'Thor(A)'` | 10.1a, 11.0a |
| `'Blackwell'` | 10.0, 10.3, 12.0, 12.1 |
| `'Blackwell(A)'` | 10.0a, 10.3a, 12.0a, 12.1a |
The `'(A)'` names select the architecture-specific (`a`-suffixed) compute
capabilities of their family. Code built for these can use
architecture-specific features, but runs only on exactly that architecture:
it has no forward compatibility, so it cannot be combined with `+PTX`.
The family-specific `f`-suffixed compute capabilities introduced with
CUDA 12.9 (e.g. `10.0f`, which runs on all later members of the same
family) may be requested explicitly, but are not part of any named set.
*Note:* the `f`-suffixed capabilities only benefit kernels that use
architecture-conditional features (such as the 5th-generation tensor core
instructions), which plain compute capabilities cannot express. For all
other code they gain nothing: a plain capability plus `+PTX` is compatible
*beyond* the family, and NVCC refuses to combine `X.Yf` with plain `X.Y`
in the same build. Prefer the plain capabilities unless you know your
kernels require family-specific features.
Examples:
+286 -219
View File
@@ -7,12 +7,14 @@ import dataclasses
import re
import typing as T
from .. import mlog
from ..mesonlib import listify, version_compare
from ..compilers.cuda import CudaCompiler
from ..interpreter.type_checking import NoneType
from . import NewExtensionModule, ModuleInfo
from ..utils.universal import Version
from ..interpreterbase import (
ContainerTypeInfo, InvalidArguments, KwargInfo, noKwargs, typed_kwargs, typed_pos_args,
)
@@ -33,7 +35,7 @@ if T.TYPE_CHECKING:
DETECTED_KW: KwargInfo[T.Union[None, T.List[str]]] = KwargInfo('detected', (ContainerTypeInfo(list, str), NoneType), listify=True)
@dataclasses.dataclass
@dataclasses.dataclass(slots=True)
class _CudaVersion:
meson: str
@@ -48,6 +50,12 @@ class _CudaVersion:
# Copied from: https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#id7
_DRIVER_TABLE_VERSION: T.List[_CudaVersion] = [
_CudaVersion('13.3.1', 'unknown', '610.43.02'),
_CudaVersion('13.3.0', 'unknown', '610.43.02'),
_CudaVersion('13.2.1', 'unknown', '595.58.03'),
_CudaVersion('13.2.0', 'unknown', '595.45.04'),
_CudaVersion('13.1.1', 'unknown', '590.48.01'),
_CudaVersion('13.1.0', 'unknown', '590.44.01'),
_CudaVersion('13.0.2', 'unknown', '580.95.05'),
_CudaVersion('13.0.1', 'unknown', '580.82.07'),
_CudaVersion('13.0.0', 'unknown', '580.65.06'),
@@ -104,6 +112,116 @@ _DRIVER_TABLE_VERSION: T.List[_CudaVersion] = [
_CudaVersion('7.0.28', '347.62', '346.46'),
]
@dataclasses.dataclass(slots=True)
class _IsaDef:
# half-open range, i.e., support for '2.0'/sm_20 is included in CUDA 3.0 <= cuda_version < 9.0
min_cuda_ver: str # included
max_cuda_ver: T.Optional[str] # excluded; None = still supported
common: bool # considered "common" for when user passes 'Common' arg
virt: T.Optional[str] = None # virtual arch, if it differs from the code arch (sm_21 has no compute_21)
def cuda_too_old(self, cuda_version: str) -> bool:
return version_compare(cuda_version, '<' + self.min_cuda_ver)
def arch_too_old(self, cuda_version: str) -> bool:
return self.max_cuda_ver is not None and version_compare(cuda_version, '>=' + self.max_cuda_ver)
def supports(self, cuda_version: str) -> bool:
return not self.cuda_too_old(cuda_version) and not self.arch_too_old(cuda_version)
# Replicates https://en.wikipedia.org/wiki/CUDA#GPUs_supported
# a max_cuda_ver of None denotes an arch that is still fully supported as of 2026-07-13
_MICROISA_RANGES_DEF: T.Mapping[str, _IsaDef] = {
### Fermi
'2.0': _IsaDef( '3.0', '9.0', False), # GF100/GF110
'2.1': _IsaDef( '3.2', '9.0', False, virt='2.0'), # GF104/GF106/GF108/GF114/GF116/GF117/GF119
### Kepler
'3.0': _IsaDef( '5.0', '11.0', True), # GK104/GK106/GK107
'3.2': _IsaDef( '6.0', '11.0', False), # GK20A (Tegra/Jetson K1)
'3.5': _IsaDef( '5.0', '12.0', True), # GK110/GK208
'3.7': _IsaDef( '6.5', '12.0', False), # GK210 (Tesla K80)
### Maxwell
'5.0': _IsaDef( '6.5', '13.0', True), # GM107/GM108
'5.2': _IsaDef( '6.5', '13.0', True), # GM200/GM204/GM206
'5.3': _IsaDef( '6.5', '13.0', False), # GM20B (Tegra/Jetson X1)
### Pascal
'6.0': _IsaDef( '8.0', '13.0', True), # GP100
'6.1': _IsaDef( '8.0', '13.0', True), # GP102/GP104/GP106/GP107/GP108
'6.2': _IsaDef( '8.0', '13.0', False), # GP10B (Tegra/Jetson X2)
### Volta
'7.0': _IsaDef( '9.0', '13.0', True), # GV100
'7.2': _IsaDef('10.0', '13.0', False), # GV10B/GV11B (Tegra/Jetson Xavier)
### Turing
'7.5': _IsaDef('10.0', None, True), # TU102/TU104/TU106/TU116/TU117
### Ampere
'8.0': _IsaDef('11.0', None, True), # GA100
'8.6': _IsaDef('11.1', None, True), # GA102/GA103/GA104/GA106/GA107
'8.7': _IsaDef('11.5', None, False), # GA10B (Jetson Orin)
'8.8': _IsaDef('13.0', None, False), # undocumented Ampere variant
### Lovelace
'8.9': _IsaDef('11.8', None, True), # AD102/AD103/AD104/AD106/AD107
### Hopper
'9.0a': _IsaDef('12.0', None, False), # GH100 (no minor backcompat)
'9.0': _IsaDef('11.8', None, True), # GH100
### Blackwell
# CUDA 12.9 added 'X.Yf' specifiers
# https://developer.nvidia.com/blog/nvidia-blackwell-and-nvidia-cuda-12-9-introduce-family-specific-architecture-features/
'10.0a': _IsaDef('12.8', None, False), # GB100 (no minor backcompat)
'10.0f': _IsaDef('12.9', None, False), # GB100
'10.0': _IsaDef('12.8', None, True), # GB100
'10.1a': _IsaDef('12.8', '13.0', False), # GB10B (Jetson Thor) (changed to 11.0 in CUDA 13) (no minor backcompat)
'10.1f': _IsaDef('12.9', '13.0', False), # GB10B (Jetson Thor) (changed to 11.0 in CUDA 13)
'10.1': _IsaDef('12.8', '13.0', False), # GB10B (Jetson Thor) (changed to 11.0 in CUDA 13)
'10.3a': _IsaDef('12.9', None, False), # GB110 (no minor backcompat)
'10.3f': _IsaDef('12.9', None, False), # GB110
'10.3': _IsaDef('12.9', None, True), # GB110
'11.0a': _IsaDef('13.0', None, False), # GB10B (Jetson Thor) (no minor backcompat)
'11.0f': _IsaDef('13.0', None, False), # GB10B (Jetson Thor)
'11.0': _IsaDef('13.0', None, False), # GB10B (Jetson Thor)
'12.0a': _IsaDef('12.9', None, False), # GB100/GB202/GB203/GB205/GB206/GB207 (no minor backcompat)
'12.0f': _IsaDef('12.9', None, False), # GB100/GB202/GB203/GB205/GB206/GB207
'12.0': _IsaDef('12.8', None, True), # GB100/GB202/GB203/GB205/GB206/GB207
'12.1a': _IsaDef('12.9', None, False), # GB20B (no minor backcompat)
'12.1f': _IsaDef('12.9', None, False), # GB20B
'12.1': _IsaDef('12.9', None, True), # GB20B
}
_FAMILY_TO_MICROISAS: T.Mapping[str, T.FrozenSet[str]] = {
'Fermi': frozenset(['2.0', '2.1']),
'Kepler': frozenset(['3.0', '3.5']),
'Kepler+Tegra': frozenset(['3.2']),
'Kepler+Tesla': frozenset(['3.7']),
'Maxwell': frozenset(['5.0', '5.2']),
'Maxwell+Tegra': frozenset(['5.3']),
'Pascal': frozenset(['6.0', '6.1']),
'Pascal+Tegra': frozenset(['6.2']),
'Volta': frozenset(['7.0']),
'Xavier': frozenset(['7.2']),
'Turing': frozenset(['7.5']),
'Ampere': frozenset(['8.0', '8.6']),
'Orin': frozenset(['8.7']),
'Lovelace': frozenset(['8.9']),
'Hopper': frozenset(['9.0']),
'Hopper(A)': frozenset(['9.0a']),
'Thor': frozenset(['10.1', '11.0']),
'Thor(A)': frozenset(['10.1a', '11.0a']),
'Blackwell': frozenset(['10.0', '10.3', '12.0', '12.1']),
'Blackwell(A)': frozenset(['10.0a', '10.3a', '12.0a', '12.1a']),
}
# reverse lookup for family-specific ('f') pairing checks; '(A)' pseudo-families excluded
_ISA_TO_FAMILY: T.Mapping[str, str] = {isa: family
for family, isas in _FAMILY_TO_MICROISAS.items() if not family.endswith('(A)')
for isa in isas}
# a single micro-ISA, e.g. '8.6', '9.0a' or '10.0f'
_MICROISA_RE = re.compile(r'[0-9]+\.[0-9][af]?')
# an output micro-ISA with an optional virtual micro-ISA, e.g. '12.0a' or '8.6(8.0)'
_ARCH_SPEC_RE = re.compile(rf'({_MICROISA_RE.pattern})(?:\(({_MICROISA_RE.pattern})\))?')
class CudaModule(NewExtensionModule):
INFO = ModuleInfo('CUDA', '0.50.0', unstable=True)
@@ -186,261 +304,210 @@ class CudaModule(NewExtensionModule):
return cuda_version, arch_list, detected
def _filter_cuda_arch_list(self, cuda_arch_list: T.List[str], lo: str, hi: T.Optional[str], saturate: str) -> T.List[str]:
"""
Filter CUDA arch list (no codenames) for >= low and < hi architecture
bounds, and deduplicate.
Architectures >= hi are replaced with saturate.
"""
filtered_cuda_arch_list = []
for arch in cuda_arch_list:
if arch:
if lo and version_compare(arch, '<' + lo):
continue
if hi and version_compare(arch, '>=' + hi):
arch = saturate
if arch not in filtered_cuda_arch_list:
filtered_cuda_arch_list.append(arch)
return filtered_cuda_arch_list
def _nvcc_arch_flags(self, cuda_version: str, cuda_arch_list: AutoArch, detected: T.List[str]) -> T.Tuple[T.List[str], T.List[str]]:
@staticmethod
def _nvcc_arch_flags(cuda_version: str, cuda_arch_list: AutoArch, detected: T.List[str]) -> T.Tuple[T.List[str], T.List[str]]:
"""
Using the CUDA Toolkit version and the target architectures, compute
the NVCC architecture flags.
"""
# Replicates much of the logic of
# https://github.com/Kitware/CMake/blob/master/Modules/FindCUDA/select_compute_arch.cmake
# except that a bug with cuda_arch_list="All" is worked around by
# tracking both lower and upper limits on GPU architectures.
# arches the current nvcc supports
cuda_supported_gpu_architectures: T.Set[str] = set()
# arches you get when asking for 'All'
cuda_known_gpu_architectures: T.Set[str] = set()
# arches you get when asking for 'Common'
cuda_common_gpu_architectures: T.Set[str] = set()
# maximum common arch (used as the PTX saturation target)
cuda_max_arch: str = '1.0'
cuda_known_gpu_architectures = [] # noqa: E221
cuda_common_gpu_architectures = ['3.0', '3.5', '5.0'] # noqa: E221
cuda_hi_limit_gpu_architecture = None # noqa: E221
cuda_lo_limit_gpu_architecture = '2.0' # noqa: E221
cuda_all_gpu_architectures = ['3.0', '3.2', '3.5', '5.0'] # noqa: E221
for arch, definition in _MICROISA_RANGES_DEF.items():
if definition.supports(cuda_version):
cuda_supported_gpu_architectures.add(arch)
# Fermi and Kepler support have been dropped since 12.0
if version_compare(cuda_version, '<12.0'):
cuda_known_gpu_architectures.extend(['Fermi', 'Kepler'])
if arch.endswith('f'):
# don't want 'X.Yf' arches as part of any default collection
continue
# Everything older than Turing is dropped by 13.0
if version_compare(cuda_version, '<13.0'):
cuda_known_gpu_architectures.append('Maxwell')
cuda_known_gpu_architectures.add(arch)
if version_compare(cuda_version, '<7.0'):
cuda_hi_limit_gpu_architecture = '5.2'
if arch.endswith('a'):
# don't perform version comparisons on 'X.Ya' arches
continue
if version_compare(cuda_version, '>=7.0'):
cuda_known_gpu_architectures += ['Kepler+Tegra', 'Kepler+Tesla', 'Maxwell+Tegra'] # noqa: E221
cuda_common_gpu_architectures += ['5.2'] # noqa: E221
if definition.common:
cuda_common_gpu_architectures.add(arch)
if version_compare(cuda_version, '<8.0'):
cuda_common_gpu_architectures += ['5.2+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '6.0' # noqa: E221
if Version(arch) > Version(cuda_max_arch):
cuda_max_arch = arch
if version_compare(cuda_version, '>=8.0'):
cuda_known_gpu_architectures += ['Pascal', 'Pascal+Tegra'] # noqa: E221
cuda_common_gpu_architectures += ['6.0', '6.1'] # noqa: E221
cuda_all_gpu_architectures += ['6.0', '6.1', '6.2'] # noqa: E221
if version_compare(cuda_version, '<9.0'):
cuda_common_gpu_architectures += ['6.1+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '7.0' # noqa: E221
if version_compare(cuda_version, '>=9.0'):
cuda_known_gpu_architectures += ['Volta', 'Xavier'] # noqa: E221
cuda_common_gpu_architectures += ['7.0'] # noqa: E221
cuda_all_gpu_architectures += ['7.0', '7.2'] # noqa: E221
# https://docs.nvidia.com/cuda/archive/9.0/cuda-toolkit-release-notes/index.html#unsupported-features
cuda_lo_limit_gpu_architecture = '3.0' # noqa: E221
if version_compare(cuda_version, '<10.0'):
cuda_common_gpu_architectures += ['7.2+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '8.0' # noqa: E221
if version_compare(cuda_version, '>=10.0'):
cuda_known_gpu_architectures += ['Turing'] # noqa: E221
cuda_common_gpu_architectures += ['7.5'] # noqa: E221
cuda_all_gpu_architectures += ['7.5'] # noqa: E221
if version_compare(cuda_version, '<11.0'):
cuda_common_gpu_architectures += ['7.5+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '8.0' # noqa: E221
# need to account for the fact that Ampere is commonly assumed to include
# SM8.0 and SM8.6 even though CUDA 11.0 doesn't support SM8.6
cuda_ampere_bin = ['8.0']
cuda_ampere_ptx = ['8.0']
if version_compare(cuda_version, '>=11.0'):
cuda_known_gpu_architectures += ['Ampere'] # noqa: E221
cuda_common_gpu_architectures += ['8.0'] # noqa: E221
cuda_all_gpu_architectures += ['8.0'] # noqa: E221
# https://docs.nvidia.com/cuda/archive/11.0/cuda-toolkit-release-notes/index.html#deprecated-features
cuda_lo_limit_gpu_architecture = '3.5' # noqa: E221
if version_compare(cuda_version, '<11.1'):
cuda_common_gpu_architectures += ['8.0+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '8.6' # noqa: E221
if version_compare(cuda_version, '>=11.1'):
cuda_ampere_bin += ['8.6'] # noqa: E221
cuda_ampere_ptx = ['8.6'] # noqa: E221
cuda_common_gpu_architectures += ['8.6'] # noqa: E221
cuda_all_gpu_architectures += ['8.6'] # noqa: E221
if version_compare(cuda_version, '<11.8'):
cuda_common_gpu_architectures += ['8.6+PTX'] # noqa: E221
cuda_hi_limit_gpu_architecture = '8.7' # noqa: E221
if version_compare(cuda_version, '>=11.8'):
cuda_known_gpu_architectures += ['Orin', 'Lovelace', 'Hopper'] # noqa: E221
cuda_common_gpu_architectures += ['8.9', '9.0', '9.0+PTX'] # noqa: E221
cuda_all_gpu_architectures += ['8.7', '8.9', '9.0'] # noqa: E221
if version_compare(cuda_version, '<12'):
cuda_hi_limit_gpu_architecture = '9.1' # noqa: E221
if version_compare(cuda_version, '>=12.0'):
# https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#deprecated-features (Current)
# https://docs.nvidia.com/cuda/archive/12.0/cuda-toolkit-release-notes/index.html#deprecated-features (Eventual?)
cuda_lo_limit_gpu_architecture = '5.0' # noqa: E221
if version_compare(cuda_version, '<13'):
cuda_hi_limit_gpu_architecture = '10.0' # noqa: E221
if version_compare(cuda_version, '>=12.8'):
cuda_known_gpu_architectures.append('Blackwell')
cuda_common_gpu_architectures.extend(['10.0', '12.0'])
cuda_all_gpu_architectures.extend(['10.0', '12.0'])
if version_compare(cuda_version, '<13'):
# Yes, 12.8 and 12.9 support 10.1, but 13.0 doesn't
cuda_common_gpu_architectures.append('10.1')
cuda_all_gpu_architectures.append('10.1')
cuda_hi_limit_gpu_architecture = '12.1'
if version_compare(cuda_version, '>=12.9'):
cuda_common_gpu_architectures.extend(['10.3', '12.1'])
cuda_all_gpu_architectures.extend(['10.3', '12.1'])
if version_compare(cuda_version, '>=13.0'):
cuda_common_gpu_architectures.append('11.0')
cuda_all_gpu_architectures.append('11.0')
cuda_lo_limit_gpu_architecture = '7.5'
if version_compare(cuda_version, '<14'):
cuda_hi_limit_gpu_architecture = '12.1'
# need to add '+PTX' for the 'Common' collection
cuda_common_gpu_architectures.add(cuda_max_arch + '+PTX')
if not cuda_arch_list:
cuda_arch_list = 'Auto'
archs: T.Iterable[str]
if cuda_arch_list == 'All': # noqa: E271
cuda_arch_list = cuda_known_gpu_architectures
archs = cuda_known_gpu_architectures
elif cuda_arch_list == 'Common': # noqa: E271
cuda_arch_list = cuda_common_gpu_architectures
archs = cuda_common_gpu_architectures
elif cuda_arch_list == 'Auto': # noqa: E271
if detected:
if isinstance(detected, list):
cuda_arch_list = detected
# a detected GPU newer than the toolkit supports can still JIT PTX
# for the newest common arch -> saturate instead of dropping
saturated: T.List[str] = []
for arch in detected:
if _MICROISA_RE.fullmatch(arch) and Version(arch) > Version(cuda_max_arch):
saturated.append(cuda_max_arch + '+PTX')
else:
cuda_arch_list = self._break_arch_string(detected)
cuda_arch_list = self._filter_cuda_arch_list(cuda_arch_list,
cuda_lo_limit_gpu_architecture,
cuda_hi_limit_gpu_architecture,
cuda_common_gpu_architectures[-1])
saturated.append(arch)
archs = saturated
else:
cuda_arch_list = cuda_common_gpu_architectures
archs = cuda_common_gpu_architectures
elif isinstance(cuda_arch_list, str):
cuda_arch_list = self._break_arch_string(cuda_arch_list)
cuda_arch_list = sorted(x for x in set(cuda_arch_list) if x)
cuda_arch_bin: T.List[str] = []
cuda_arch_ptx: T.List[str] = []
for arch_name in cuda_arch_list:
arch_bin: T.Optional[T.List[str]]
arch_ptx: T.Optional[T.List[str]]
add_ptx = arch_name.endswith('+PTX')
if add_ptx:
arch_name = arch_name[:-len('+PTX')]
if re.fullmatch('[0-9]+\\.[0-9](\\([0-9]+\\.[0-9]\\))?', arch_name):
arch_bin, arch_ptx = [arch_name], [arch_name]
archs = CudaModule._break_arch_string(cuda_arch_list)
else:
arch_bin, arch_ptx = {
'Fermi': (['2.0', '2.1(2.0)'], []),
'Kepler+Tegra': (['3.2'], []),
'Kepler+Tesla': (['3.7'], []),
'Kepler': (['3.0', '3.5'], ['3.5']),
'Maxwell+Tegra': (['5.3'], []),
'Maxwell': (['5.0', '5.2'], ['5.2']),
'Pascal': (['6.0', '6.1'], ['6.1']),
'Pascal+Tegra': (['6.2'], []),
'Volta': (['7.0'], ['7.0']),
'Xavier': (['7.2'], []),
'Turing': (['7.5'], ['7.5']),
'Ampere': (cuda_ampere_bin, cuda_ampere_ptx),
'Orin': (['8.7'], []),
'Lovelace': (['8.9'], ['8.9']),
'Hopper': (['9.0'], ['9.0']),
'Blackwell': (['10.0'], ['10.0']),
}.get(arch_name, (None, None))
archs = cuda_arch_list
if arch_bin is None:
raise InvalidArguments(f'Unknown CUDA Architecture Name {arch_name}!')
# shared validation for the real and virtual halves of an 'X.Y(Z.W)' arch
def isa_usable(isa: str, kind: str, arch: str) -> bool:
isadef = _MICROISA_RANGES_DEF.get(isa, None)
if isadef is None:
raise InvalidArguments(f'Unknown CUDA {kind} in {arch}!')
if isadef.cuda_too_old(cuda_version):
mlog.warning(f'CUDA {cuda_version} is too old for architecture {isa}')
return False
if isadef.arch_too_old(cuda_version):
mlog.warning(f'Architecture {isa} is too old for CUDA {cuda_version}')
return False
assert isa in cuda_supported_gpu_architectures
return True
cuda_arch_bin += arch_bin
# nvcc emits one SASS slot per GPU code and refuses to fill the same slot from
# both a family-specific and a non family-specific arch ("The same GPU code
# (`sm_121`) generated for non family-specific and family-specific GPU arch").
# 'X.Yf' occupies the same slot as 'X.Y', while 'X.Ya' is a slot of its own.
# slot -> (family-specific?, spec of first request, {(virtual, output), ...})
cuda_arch_bin: T.Dict[str, T.Tuple[bool, str, T.Set[T.Tuple[Version, Version]]]] = {}
# PTX is embedded per virtual arch and never occupies a SASS slot
cuda_arch_ptx: T.Set[Version] = set()
def add_bin_target(virtarch: str, outarch: str) -> None:
slot = outarch.rstrip('f')
family_specific = outarch.endswith('f') or virtarch.endswith('f')
# remember how the slot was requested (in input syntax), so that a later
# conflicting request can name both offenders in its error message
spec = outarch if virtarch == outarch else f'{outarch}({virtarch})'
if slot not in cuda_arch_bin:
cuda_arch_bin[slot] = (family_specific, spec, set())
elif cuda_arch_bin[slot][0] != family_specific:
specs = ' and '.join(sorted((cuda_arch_bin[slot][1], spec)))
gpu_code = 'sm_' + slot.replace('.', '')
raise InvalidArguments(f'CUDA archs {specs} generate the same GPU code {gpu_code} from a family-specific and a non family-specific arch!')
cuda_arch_bin[slot][2].add((Version(virtarch), Version(outarch)))
for arch in archs:
add_ptx = arch.endswith('+PTX')
if add_ptx:
if not arch_ptx:
arch_ptx = arch_bin
cuda_arch_ptx += arch_ptx
arch = arch[:-len('+PTX')]
cuda_arch_bin = sorted(set(cuda_arch_bin))
cuda_arch_ptx = sorted(set(cuda_arch_ptx))
if arch in _FAMILY_TO_MICROISAS:
# something like 'Maxwell' or 'Ampere'
# '(A)' arches have no forward compatibility, so embedding their PTX is meaningless
if add_ptx and arch.endswith('(A)'):
raise InvalidArguments(f'CUDA arch {arch} and +PTX are mutually exclusive')
nvcc_flags = []
nvcc_archs_readable = []
for arch in cuda_arch_bin:
arch, codev = re.fullmatch(
'([0-9]+\\.[0-9])(?:\\(([0-9]+\\.[0-9])\\))?', arch).groups()
if version_compare(arch, '<' + cuda_lo_limit_gpu_architecture):
continue
if cuda_hi_limit_gpu_architecture and version_compare(arch, '>=' + cuda_hi_limit_gpu_architecture):
continue
if codev:
arch = arch.replace('.', '')
codev = codev.replace('.', '')
nvcc_flags += ['-gencode', 'arch=compute_' + codev + ',code=sm_' + arch]
nvcc_archs_readable += ['sm_' + arch]
isas = _FAMILY_TO_MICROISAS[arch]
intersection = isas & cuda_supported_gpu_architectures
if intersection:
for realarch in sorted(intersection):
virtarch = _MICROISA_RANGES_DEF[realarch].virt or realarch
add_bin_target(virtarch, realarch)
if add_ptx:
# we don't want PTX for things like 'Kepler+Tegra'
maxarch = max(intersection, key=Version)
virtarch = _MICROISA_RANGES_DEF[maxarch].virt or maxarch
cuda_arch_ptx.add(Version(virtarch))
elif all(_MICROISA_RANGES_DEF[isa].cuda_too_old(cuda_version) for isa in isas):
mlog.warning(f'CUDA {cuda_version} is too old for {arch}')
elif all(_MICROISA_RANGES_DEF[isa].arch_too_old(cuda_version) for isa in isas):
mlog.warning(f'{arch} is too old for CUDA {cuda_version}')
else:
arch = arch.replace('.', '')
nvcc_flags += ['-gencode', 'arch=compute_' + arch + ',code=sm_' + arch]
nvcc_archs_readable += ['sm_' + arch]
mlog.warning(f'{arch} is not supported by CUDA {cuda_version}')
elif m := _ARCH_SPEC_RE.fullmatch(arch):
# something like '12.0a' or '8.6(8.0)'
realarch, virtarch = m.groups()
for arch in cuda_arch_ptx:
arch, codev = re.fullmatch(
'([0-9]+\\.[0-9])(?:\\(([0-9]+\\.[0-9])\\))?', arch).groups()
# 'X.Ya' arches have no forward compatibility, so embedding their PTX is meaningless
if add_ptx and (virtarch or realarch).endswith('a'):
raise InvalidArguments(f'CUDA arch {arch} and +PTX are mutually exclusive')
if codev:
arch = codev
# an architecture-specific 'X.Ya' virtual arch can only emit code for exactly 'X.Ya'
# (nvcc: "Incompatible code generation requested")
if virtarch and virtarch.endswith('a') and virtarch != realarch:
raise InvalidArguments(f'CUDA virtual arch {virtarch} is architecture-specific and can only pair with {virtarch} in {arch}!')
if version_compare(arch, '<' + cuda_lo_limit_gpu_architecture):
continue
if cuda_hi_limit_gpu_architecture and version_compare(arch, '>=' + cuda_hi_limit_gpu_architecture):
if not isa_usable(realarch, 'Architecture', arch):
continue
arch = arch.replace('.', '')
nvcc_flags += ['-gencode', 'arch=compute_' + arch + ',code=compute_' + arch]
nvcc_archs_readable += ['compute_' + arch]
if virtarch:
if not isa_usable(virtarch, 'Virtual Architecture', arch):
continue
# an ISA carrying a virt override has no virtual form of its own (there is no compute_21)
redirect = _MICROISA_RANGES_DEF[virtarch].virt
if redirect is not None:
raise InvalidArguments(f'CUDA virtual arch {virtarch} does not exist in {arch}, use {redirect} instead!')
return nvcc_flags, nvcc_archs_readable
# nvcc pairing rules for suffixed arches ("Incompatible code generation requested"):
# - a family-specific 'X.Yf' virtual arch pairs only with members of its own
# family generation that are at least as new as itself
# - a family-specific 'X.Yf' code arch requires a same-generation virtual arch
realbase = realarch.rstrip('af')
realfamily = _ISA_TO_FAMILY.get(realbase, 'unknown')
if virtarch.endswith('f'):
virtbase = virtarch.rstrip('f')
virtfamily = _ISA_TO_FAMILY.get(virtbase, 'unknown')
if (virtbase.split('.')[0] != realbase.split('.')[0]
or virtfamily != realfamily
or Version(realbase) < Version(virtbase)):
raise InvalidArguments(f'CUDA virtual arch {virtarch} ({virtfamily}) is family-specific and cannot pair with {realarch} ({realfamily}) in {arch}!')
elif realarch.endswith('f') and virtarch.split('.')[0] != realbase.split('.')[0]:
virtfamily = _ISA_TO_FAMILY.get(virtarch.rstrip('af'), 'unknown')
raise InvalidArguments(f'CUDA family-specific arch {realarch} ({realfamily}) requires a same-generation virtual arch, not {virtarch} ({virtfamily}), in {arch}!')
else:
virtarch = _MICROISA_RANGES_DEF[realarch].virt or realarch
add_bin_target(virtarch, realarch)
if add_ptx:
cuda_arch_ptx.add(Version(virtarch))
else:
raise InvalidArguments(f'Unknown CUDA Architecture Name {arch}!')
# the order we're looking for:
# - 12.0a < 12.0f < 12.0
# by always appending 'z' to the Version, the vanilla '12.0' always goes last
def version_key(v: Version) -> T.Tuple[T.Union[int, str], ...]:
return (*v, 'z')
bin_pairs: T.List[T.Tuple[Version, Version]] = [pair for _, _, pairs in cuda_arch_bin.values() for pair in pairs]
# binary code for each requested arch, with the PTX fallbacks at the end
gencode_flags: T.List[str] = []
for virtual_target, output_target in sorted(bin_pairs, key=lambda p: (version_key(p[0]), version_key(p[1]))):
virt = str(virtual_target).replace('.', '')
output = str(output_target).replace('.', '')
gencode_flags += ['-gencode', f'arch=compute_{virt},code=sm_{output}']
for virtual_target in sorted(cuda_arch_ptx, key=version_key):
virt = str(virtual_target).replace('.', '')
gencode_flags += ['-gencode', f'arch=compute_{virt},code=compute_{virt}']
arch_names: T.List[str] = []
for _, output_target in sorted(bin_pairs, key=lambda p: version_key(p[1])):
arch_names.append('sm_' + str(output_target).replace('.', ''))
for virtual_target in sorted(cuda_arch_ptx, key=version_key):
arch_names.append('compute_' + str(virtual_target).replace('.', ''))
return gencode_flags, arch_names
def initialize(interp: Interpreter) -> CudaModule:
return CudaModule(interp)
+8 -2
View File
@@ -969,9 +969,9 @@ class Version:
# extract numeric and alphabetic sequences
# numeric sequences are converted from strings to ints
self._v = [
self._v: T.Tuple[T.Union[int, str], ...] = tuple(
int(m.group(1)) if m.group(1) else m.group(2)
for m in _VERSION_TOK_RE.finditer(s)]
for m in _VERSION_TOK_RE.finditer(s))
def __str__(self) -> str:
return self._s
@@ -1009,6 +1009,12 @@ class Version:
return self._v != other._v
return NotImplemented
def __hash__(self) -> int:
return hash(self._v)
def __iter__(self) -> T.Iterator[int | str]:
return iter(self._v)
def __cmp(self, other: 'Version', comparator: T.Callable[[T.Any, T.Any], bool]) -> bool:
# compare each sequence in order
for ours, theirs in zip(self._v, other._v):
+79
View File
@@ -24,6 +24,7 @@ import mesonbuild.dependencies.base
import mesonbuild.dependencies.factory
import mesonbuild.envconfig
import mesonbuild.environment
import mesonbuild.modules.cuda
import mesonbuild.modules.gnome
import mesonbuild.scripts.env2mfile
from mesonbuild import coredata
@@ -2240,3 +2241,81 @@ class InternalTests(unittest.TestCase):
self.assertEqual(actual.compile_args, expected.compile_args)
self.assertEqual(actual.link_args, expected.link_args)
self.assertEqual(actual.cmake, expected.cmake)
def test_cuda_module_nvcc_arch_flags(self):
def flags(cuda_version, arch_list, detected=None):
# swallow the mlog warnings emitted for filtered-out archs
with contextlib.redirect_stdout(io.StringIO()):
return mesonbuild.modules.cuda.CudaModule._nvcc_arch_flags(cuda_version, arch_list, detected or [])
# (cuda_version, arch_list, detected, expected gencode flags, expected readable names)
cases = [
# baseline; also asserted in "test cases/cuda/3 cudamodule/meson.build"
('11.1', '8.6', None, ['-gencode', 'arch=compute_86,code=sm_86'], ['sm_86']),
# toolkit too old for the arch -> filtered out with a warning
('11.0', '8.6', None, [], []),
# family names only expand to members inside the support window
('11.0', 'Ampere', None, ['-gencode', 'arch=compute_80,code=sm_80'], ['sm_80']),
# 'X.Y(Z.W)+PTX' embeds the PTX of the virtual arch, not the real one
('11.1', '8.6(8.0)+PTX', None, ['-gencode', 'arch=compute_80,code=sm_86', '-gencode', 'arch=compute_80,code=compute_80'], ['sm_86', 'compute_80']),
# a detected GPU newer than the toolkit saturates to the max common arch + PTX
('10.2', 'Auto', ['8.0'], ['-gencode', 'arch=compute_75,code=sm_75', '-gencode', 'arch=compute_75,code=compute_75'], ['sm_75', 'compute_75']),
# sm_21 has no compute_21: both the family and the numeric spelling must fall back to compute_20
('8.0', 'Fermi', None, ['-gencode', 'arch=compute_20,code=sm_20', '-gencode', 'arch=compute_20,code=sm_21'], ['sm_20', 'sm_21']),
('8.0', '2.1', None, ['-gencode', 'arch=compute_20,code=sm_21'], ['sm_21']),
# PTX fallbacks go at the end, not interleaved by virtual arch
('12.9', '5.0+PTX;8.6', None, ['-gencode', 'arch=compute_50,code=sm_50', '-gencode', 'arch=compute_86,code=sm_86', '-gencode', 'arch=compute_50,code=compute_50'], ['sm_50', 'sm_86', 'compute_50']),
# numeric (not lexicographic) ordering: 10.x < 12.x
('12.9', 'Blackwell', None, ['-gencode', 'arch=compute_100,code=sm_100', '-gencode', 'arch=compute_103,code=sm_103', '-gencode', 'arch=compute_120,code=sm_120', '-gencode', 'arch=compute_121,code=sm_121'], ['sm_100', 'sm_103', 'sm_120', 'sm_121']),
('12.9', 'Thor;Hopper(A)', None, ['-gencode', 'arch=compute_90a,code=sm_90a', '-gencode', 'arch=compute_101,code=sm_101'], ['sm_90a', 'sm_101']),
# different code under different CUDA versions
('12.9', 'Thor', None, ['-gencode', 'arch=compute_101,code=sm_101'], ['sm_101']),
('13.0', 'Thor', None, ['-gencode', 'arch=compute_110,code=sm_110'], ['sm_110']),
# family-specific virtual arch pairs forward within its family
('12.9', '10.3(10.0f)', None, ['-gencode', 'arch=compute_100f,code=sm_103'], ['sm_103']),
# architecture-specific archs self-pair
('12.0', '9.0a', None, ['-gencode', 'arch=compute_90a,code=sm_90a'], ['sm_90a']),
# an 'a' code arch may be built from its own plain virtual arch
('12.9', '10.0a(10.0)', None, ['-gencode', 'arch=compute_100,code=sm_100a'], ['sm_100a']),
]
for cuda_version, arch_list, detected, expected_flags, expected_readable in cases:
with self.subTest(cuda_version=cuda_version, arch_list=arch_list, detected=detected):
actual_flags, actual_readable = flags(cuda_version, arch_list, detected)
self.assertEqual(actual_flags, expected_flags)
self.assertEqual(actual_readable, expected_readable)
def test_cuda_module_nvcc_arch_flags_invalid(self):
def flags(cuda_version, arch_list):
with contextlib.redirect_stdout(io.StringIO()):
return mesonbuild.modules.cuda.CudaModule._nvcc_arch_flags(cuda_version, arch_list, [])
# (cuda_version, arch_list, expected error message pattern)
cases = [
# 'a' archs have no forward compatibility, so embedding their PTX is meaningless
('12.0', '9.0a+PTX', 'mutually exclusive'),
('12.0', 'Hopper(A)+PTX', 'mutually exclusive'),
# an 'a' virtual arch can only emit code for exactly itself
('12.9', '10.0(10.0a)', 'architecture-specific'),
# there is no compute_21; the error points at the correct virtual arch
('8.0', '2.1(2.1)', r'use 2\.0 instead'),
# 'f' virtual archs only pair within their own family generation:
# not Thor (carved out of the 10.x family), not other majors, not backwards
('12.9', '10.1(10.0f)', 'family-specific'),
('12.9', '12.0(10.0f)', 'family-specific'),
('12.9', '10.0(10.3f)', 'family-specific'),
# an 'f' code arch requires a same-generation virtual arch
('12.9', '10.0f(9.0)', 'same-generation'),
# nvcc: "The same GPU code (`sm_121`) generated for non family-specific
# and family-specific GPU arch"
('12.9', '12.1f;12.1', r'same GPU code sm_121'),
# ... also when the plain arch comes from a named set expansion
('12.9', 'Blackwell;12.1f', r'same GPU code sm_121'),
# unknown archs
('12.9', '99.9', 'Unknown CUDA'),
('12.9', '8.6(99.9)', 'Unknown CUDA Virtual'),
('12.9', 'NotAnArch', 'Unknown CUDA Architecture Name'),
]
for cuda_version, arch_list, message in cases:
with self.subTest(cuda_version=cuda_version, arch_list=arch_list):
with self.assertRaisesRegex(InvalidArguments, message):
flags(cuda_version, arch_list)