Skip to content

Key CUDA-capability lookup by device index instead of process-wide lru_cache #778

Description

@AmirF194

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions