Description
_find_capability() in kernels/src/kernels/layer/repos.py (used by both
_CUDARepos.repos and _ROCMRepos.repos to pick which LayerRepository a kernelize()
call should dispatch to) is a bare @functools.lru_cache function with no arguments:
@lru_cache
def _find_capability() -> int:
import torch
major, minor = torch.cuda.get_device_capability(device=None)
return major * 10 + minor
Since it takes no arguments and is never invalidated, it memoizes whatever
torch.cuda.get_device_capability(device=None) returns on its first call for the
rest of the process, and device=None means "whatever CUDA device is current right
now". Any process that kernelizes modules living on more than one physical GPU of
different compute capability in one process, for example a model sharded across
heterogeneous GPUs with device_map="auto", or any code that calls
torch.cuda.set_device(n) between two kernelize() calls targeting different
devices, gets the wrong capability (and therefore the wrong LayerRepository) for
every device after the first one queried. Nothing errors or warns.
I traced this by reading repos.py / kernelize.py / layer.py and confirmed
kernelize() never passes a specific device index into the capability lookup either
(_find_device only derives a device type). This looks related to the intent behind
CUDAProperties.min_capability/max_capability that @danieldk described in #707
("the kernel mapping for kernelize supports CUDAProperties which can indicate the
capabilities supported by that kernel for that particular mapping") since that whole
mechanism silently breaks once more than one capability shows up in a process.
Steps to reproduce
I don't have access to a multi-GPU box, so I reproduced the caching behavior directly
against the real repos.py at HEAD (8ac69fc5) with torch.cuda.get_device_capability
mocked to return two different values on successive calls (standing in for
torch.cuda.set_device() switching between two physical GPUs):
import sys, types, importlib.util, importlib.machinery
fake_torch = types.ModuleType("torch")
fake_torch.__spec__ = importlib.machinery.ModuleSpec("torch", loader=None)
fake_cuda = types.ModuleType("torch.cuda")
state = {"capability": (7, 5)}
fake_cuda.get_device_capability = lambda device=None: state["capability"]
fake_torch.cuda = fake_cuda
sys.modules["torch"], sys.modules["torch.cuda"] = fake_torch, fake_cuda
# ... load the real kernels.layer.repos module ...
cuda_repos = repos._CUDARepos()
cuda_repos.insert(repos.Device(type="cuda", properties=repos.CUDAProperties(min_capability=70, max_capability=79)), {"variant": "sm75"})
cuda_repos.insert(repos.Device(type="cuda", properties=repos.CUDAProperties(min_capability=90, max_capability=99)), {"variant": "sm90"})
print(cuda_repos.repos) # {'variant': 'sm75'}, correct for cc 7.5
state["capability"] = (9, 0) # process now on a cc 9.0 device
print(cuda_repos.repos) # still {'variant': 'sm75'} -- stale
Output:
{'variant': 'sm75'}
{'variant': 'sm75'}
Expected behavior
The second call should resolve to the sm90 repository, since torch.cuda's current
device capability changed. Instead DeviceRepos.repos keeps returning the first
device's repository for the rest of the process.
Environment
- kernels version: HEAD,
8ac69fc5 (2026-08-20)
- Python version: 3.12
- PyTorch version: n/a, mocked (verified on a CPU-only host, no multi-GPU hardware
available to me to hit this with a real torch.cuda)
- CUDA version: n/a
- GPU model: n/a
- OS: Linux (Docker
python:3.12-slim)
Additional context
I only verified the caching mechanism itself (shown above, against the real
_CUDARepos/_find_capability code), not a live heterogeneous-GPU run, since I don't
have that hardware. The fix likely needs a design call between two shapes: keying
_find_capability by torch.cuda.current_device() (cheap, still wrong if a module's
own parameters aren't on the "current" device, which is the common device_map="auto"
case), or threading a resolved device index from kernelize_layer down into
DeviceRepos.repos so capability comes from the actual module being kernelized. Happy
to send a PR once there's agreement on which of those you'd want; opening this first per
CONTRIBUTING.md.
This report and the repro above were put together with the help of an LLM coding agent;
I read and verified the reasoning and the repro myself before posting.
Description
_find_capability()inkernels/src/kernels/layer/repos.py(used by both_CUDARepos.reposand_ROCMRepos.reposto pick whichLayerRepositoryakernelize()call should dispatch to) is a bare
@functools.lru_cachefunction with no arguments:Since it takes no arguments and is never invalidated, it memoizes whatever
torch.cuda.get_device_capability(device=None)returns on its first call for therest of the process, and
device=Nonemeans "whatever CUDA device is current rightnow". Any process that kernelizes modules living on more than one physical GPU of
different compute capability in one process, for example a model sharded across
heterogeneous GPUs with
device_map="auto", or any code that callstorch.cuda.set_device(n)between twokernelize()calls targeting differentdevices, gets the wrong capability (and therefore the wrong
LayerRepository) forevery device after the first one queried. Nothing errors or warns.
I traced this by reading
repos.py/kernelize.py/layer.pyand confirmedkernelize()never passes a specific device index into the capability lookup either(
_find_deviceonly derives a device type). This looks related to the intent behindCUDAProperties.min_capability/max_capabilitythat @danieldk described in #707("the kernel mapping for
kernelizesupportsCUDAPropertieswhich can indicate thecapabilities supported by that kernel for that particular mapping") since that whole
mechanism silently breaks once more than one capability shows up in a process.
Steps to reproduce
I don't have access to a multi-GPU box, so I reproduced the caching behavior directly
against the real
repos.pyat HEAD (8ac69fc5) withtorch.cuda.get_device_capabilitymocked to return two different values on successive calls (standing in for
torch.cuda.set_device()switching between two physical GPUs):Output:
Expected behavior
The second call should resolve to the
sm90repository, sincetorch.cuda's currentdevice capability changed. Instead
DeviceRepos.reposkeeps returning the firstdevice's repository for the rest of the process.
Environment
8ac69fc5(2026-08-20)available to me to hit this with a real
torch.cuda)python:3.12-slim)Additional context
I only verified the caching mechanism itself (shown above, against the real
_CUDARepos/_find_capabilitycode), not a live heterogeneous-GPU run, since I don'thave that hardware. The fix likely needs a design call between two shapes: keying
_find_capabilitybytorch.cuda.current_device()(cheap, still wrong if a module'sown parameters aren't on the "current" device, which is the common
device_map="auto"case), or threading a resolved device index from
kernelize_layerdown intoDeviceRepos.reposso capability comes from the actual module being kernelized. Happyto send a PR once there's agreement on which of those you'd want; opening this first per
CONTRIBUTING.md.
This report and the repro above were put together with the help of an LLM coding agent;
I read and verified the reasoning and the repro myself before posting.