Skip to content

Cyclic imports in cuda-bindings causing import errors #789

Closed

Description

@gflegar

Is this a duplicate?

Type of Bug

Compile-time Error

Component

cuda.bindings

Describe the bug

We are using a custom Python environment, where cyclic imports in Python do not work. This is usually not a problem for the vast majority of Python packages, but unfortunately it does cause issues when trying to use cuda-bindings, since there are some cyclic imports between the various cython files.

One example is the cycle between the driver -> _lib.utils -> driver submodules.

Would it be possible to refactor this code to remove the cycle?

How to Reproduce

Run the following code with cuda-bindings installed, which attempts to approximate our import system:

orig_import = __builtins__.__import__

import_stack = []


def import_hook(name, globals=None, locals=None, fromlist=(), *args, **kwargs):
  """Approximate a custom import system that does not allow import cycles."""

  stack_entry = (tuple(fromlist) if fromlist is not None else None, name)
  if stack_entry in import_stack:
    raise ImportError(
        f"Import cycle detected: {stack_entry}, stack: {import_stack}"
    )
  import_stack.append(stack_entry)
  res = orig_import(name, globals, locals, fromlist, *args, **kwargs)
  import_stack.pop()
  return res


__builtins__.__import__ = import_hook


import cuda.bindings.nvvm   # works
import cuda.bindings.nvrtc  # fails due to driver -> _lib.utils -> driver cycle

I get the following error on an Ubuntu system with Python 3.12.9 when running the above code:

Traceback (most recent call last):
  File "import_hook_test.py", line 24, in <module>
    import cuda.bindings.nvrtc  # fails due to driver -> _lib.utils -> driver cycle
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "import_hook_test.py", line 15, in import_hook
    res = orig_import(name, globals, locals, fromlist, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cuda/bindings/nvrtc.pyx", line 1, in init cuda.bindings.nvrtc
  File "import_hook_test.py", line 15, in import_hook
    res = orig_import(name, globals, locals, fromlist, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cuda/bindings/driver.pyx", line 1, in init cuda.bindings.driver
  File "import_hook_test.py", line 15, in import_hook
    res = orig_import(name, globals, locals, fromlist, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "cuda/bindings/_lib/utils.pyx", line 1, in init cuda.bindings._lib.utils
  File "import_hook_test.py", line 11, in import_hook
    raise ImportError(
ImportError: Import cycle detected: ((), 'cuda.bindings.driver'), stack: [(None, 'cuda.bindings.nvrtc'), ((), 'cuda.bindings.driver'), ((), 'cuda.bindings._lib.utils')]

Expected behavior

Imports with the custom hook without raising an exception.

Operating System

Custom linux-based system

nvidia-smi output

N/A - fails at import

Activity

  1. mdboom commented on Aug 12, 2025

    @mdboom
    Contributor

    👋 Hi, I'm new to the project and working on this as a good ramp-up task.

    Doing some analysis on this, these are the import cycles created when importing cuda.bindings.nvrtc:

    Image

    I think I've found a way to remove the cycle involving cuda.bindings.utils._get_handle (by having modules self-register their types in the handle map, rather than pre-generating it). That gets us something like this (which also imports fewer things by default):

    Image

    The cycle between cuda.bindings._lib.utils and cuda.bindings.driver is going to be a lot more challenging. The best I can think of would involve changing how Cython works to resolve cross-module symbols lazily after import time, but that's "non-trivial".

    I will probably pursue a patch to remove the _get_handle loop in any event, since I think it has other advantages.

  2. added a commit that references this issue on Aug 15, 2025
  3. gflegar commented on Aug 18, 2025

    @gflegar
    Author

    👋

    The cycle between cuda.bindings._lib.utils and cuda.bindings.driver is going to be a lot more challenging.

    Yeah, I was also trying to see how easy it would be to move a few things around as a patch before posting this, and this is exactly where I got stuck and figured out there's a lot of interdependencies between the two files, so moving a few things around is not super easy. (And the error message I see internally is complaining about exactly these two files depending on one another.)

  4. added a commit that references this issue on Aug 19, 2025
  5. leofang commented on Aug 23, 2025

    @leofang
    Member

    @gflegar while we're working on untangling the imports, we'd still like to understand better why this trips you, since the said circular import has been there since the beginning of this project, and it's been working fine over the years and across multiple Python versions. It seems that Cython handles the cimports properly for regular Python users, and this is different from normal Python imports.

    @oleksandr-pavlyk and I have looked into if this is related to the multi-phase initialization (I tried rebuilding the package with DCYTHON_PEP489_MULTI_PHASE_INIT=0 but found nothing different). I'd like to know:

    • Which Python interpreter do you use? (CPython, PyPy, ...)
    • What is your Python version?
    • Do you build cuda-bindings from source or pip install the binary wheel?
      • If the former, what is your Cython version?

    I am looking for ways to reproduce your Python environment without the provided import hook, so as to understand this issue better.

  6. 13 remaining items

  7. mdboom commented on Aug 27, 2025

    @mdboom
    Contributor

    Progress summary:

    3 legitimate cycles have been found when importing all of the top-level modules in cuda.bindings:

    I did find a small bug in the reproducer where it wouldn't pop modules off of the stack if they caught and handled and ImportErrors. The fix is to put the stack.pop in finally block. I don't think that makes it no longer match your custom import system (and I confirmed it doesn't seem to hide legitimate cycles), but you may want to confirm on your end.

    def import_hook(name, globals=None, locals=None, fromlist=(), *args, **kwargs):
      """Approximate a custom import system that does not allow import cycles."""
    
      stack_entry = (tuple(fromlist) if fromlist is not None else None, name)
      if stack_entry in import_stack:
        raise ImportError(
            f"Import cycle detected: {stack_entry}, stack: {import_stack}"
        )
      import_stack.append(stack_entry)
      try:
        res = orig_import(name, globals, locals, fromlist, *args, **kwargs)
      finally:
        import_stack.pop()
      return res
  8. moved this from Needs Triage to Done in CCCLon Aug 28, 2025
  9. mdboom commented on Aug 28, 2025

    @mdboom
    Contributor

    @gflegar: I believe all of the cyclical imports have been resolved at this point. Are you able to test a build from main internally to confirm that we have resolved what you need before we cut a release?

  10. added a commit that references this issue on Aug 28, 2025
    dd0ceef
  11. gflegar commented on Sep 1, 2025

    @gflegar
    Author

    Amazing, thanks!

    I was out the last couple of weeks, so I'll try to catch up on the discussion above. I'm on internal integration duty for a different project this week, so I think I'll be able to try out the new version of this early next week and let you know if everything works out.

    since the said circular import has been there since the beginning of this project, and it's been working fine over the years and across multiple Python versions

    We haven't used cuda-bindings internally in Google until now, when we started needing it since it's a dependency for CuTeDSL that we want to try out, hence we never discovered this issue.

    It seems that Cython handles the cimports properly for regular Python users, and this is different from normal Python imports.

    AFAICT, this is not a cython-specific issue for us, but any Python module that uses circular imports would have the same problem.

    @oleksandr-pavlyk and I have looked into if this is related to the multi-phase initialization (I tried rebuilding the package with DCYTHON_PEP489_MULTI_PHASE_INIT=0 but found nothing different). I'd like to know:

    I don't know the answers to all of these at the moment. We have a monorepo and a dedicated Python team that maintains a single Python environment for everyone, and it "just works" for the rest of us (until it doesn't, like what happened here 😂 ). The only thing I could find in internal documentation is that circular imports just do not work, and AFAIU this has something to do with the "Hermetic Python" setup that we use (AFAIU whenever we want to build a Python program, the entire Python interpreter and all the dependencies are built from source and packaged up together). I can try finding someone from the Python team to give you a better idea of why this is a problem, but it might take some time.

    Which Python interpreter do you use? (CPython, PyPy, ...)

    Neither, our internal runtime environment.

    What is your Python version?

    3.12

    Do you build cuda-bindings from source or pip install the binary wheel?

    From source (and we generally do not support pip & binary wheels internally).

    If the former, what is your Cython version?

    3.1.1

    Though as mentioned above, the versions themselves probably do not matter, and it's related to the custom runtime environment.

    The fix is to put the stack.pop in finally block. I don't think that makes it no longer match your custom import system (and I confirmed it doesn't seem to hide legitimate cycles), but you may want to confirm on your end.

    Ah, good catch! Yeah, that's probably correct. I wasn't trying to match the import system with the code, but just generally detect cycles, since I know that is what's not supported by it. I'll check the new version of cuda-bindings next week, and see if there's any issues remaining.

  12. gflegar commented on Sep 15, 2025

    @gflegar
    Author

    I got to try this now, and got it running. Thanks for all the work!

    I still had to apply a small patch to make everything work, due to cuda.bindings.driver importing itself through the included _lib/utils.pxi file:

    Patch

    --- a/cuda/bindings/_lib/utils.pxi.in
    +++ b/cuda/bindings/_lib/utils.pxi.in
    @@ -9,9 +9,16 @@ from libc.string cimport memcpy
     from enum import Enum as _Enum
     import ctypes as _ctypes
     cimport cuda.bindings.cydriver as cydriver
    -import cuda.bindings.driver as _driver
    +# import cuda.bindings.driver as _driver
     cimport cuda.bindings._lib.param_packer as param_packer
     
    +import functools
    +
    +@functools.cache
    +def _driver():
    +    import cuda.bindings.driver as _driver
    +    return _driver
    +
     cdef void* _callocWrapper(length, size):
         cdef void* out = calloc(length, size)
         if out is NULL:
    @@ -135,7 +142,7 @@ cdef class _HelperInputVoidPtr:
             elif isinstance(ptr, (int)):
                 # Easy run, user gave us an already configured void** address
                 self._cptr = <void*><void_ptr>ptr
    -        elif isinstance(ptr, (_driver.CUdeviceptr)):
    +        elif isinstance(ptr, (_driver().CUdeviceptr)):
                 self._cptr = <void*><void_ptr>int(ptr)
             elif PyObject_CheckBuffer(ptr):
                 # Easy run, get address from Python Buffer Protocol
    @@ -172,7 +179,7 @@ cdef class _HelperCUmemPool_attribute:
                                 {{if 'CU_MEMPOOL_ATTR_USED_MEM_CURRENT'}}cydriver.CUmemPool_attribute_enum.CU_MEMPOOL_ATTR_USED_MEM_CURRENT,{{endif}}
                                 {{if 'CU_MEMPOOL_ATTR_USED_MEM_HIGH'}}cydriver.CUmemPool_attribute_enum.CU_MEMPOOL_ATTR_USED_MEM_HIGH,{{endif}}):
                 if self._is_getter:
    -                self._cuuint64_t_val = _driver.cuuint64_t()
    +                self._cuuint64_t_val = _driver().cuuint64_t()
                     self._cptr = <void*><void_ptr>self._cuuint64_t_val.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -243,7 +250,7 @@ cdef class _HelperCUpointer_attribute:
             self._attr = attr.value
             if self._attr in ({{if 'CU_POINTER_ATTRIBUTE_CONTEXT'}}cydriver.CUpointer_attribute_enum.CU_POINTER_ATTRIBUTE_CONTEXT,{{endif}}):
                 if self._is_getter:
    -                self._ctx = _driver.CUcontext()
    +                self._ctx = _driver().CUcontext()
                     self._cptr = <void*><void_ptr>self._ctx.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -257,7 +264,7 @@ cdef class _HelperCUpointer_attribute:
             elif self._attr in ({{if 'CU_POINTER_ATTRIBUTE_DEVICE_POINTER'}}cydriver.CUpointer_attribute_enum.CU_POINTER_ATTRIBUTE_DEVICE_POINTER,{{endif}}
                                 {{if 'CU_POINTER_ATTRIBUTE_RANGE_START_ADDR'}}cydriver.CUpointer_attribute_enum.CU_POINTER_ATTRIBUTE_RANGE_START_ADDR,{{endif}}):
                 if self._is_getter:
    -                self._devptr = _driver.CUdeviceptr()
    +                self._devptr = _driver().CUdeviceptr()
                     self._cptr = <void*><void_ptr>self._devptr.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -266,7 +273,7 @@ cdef class _HelperCUpointer_attribute:
                 self._cptr = <void*>&self._void
             elif self._attr in ({{if 'CU_POINTER_ATTRIBUTE_P2P_TOKENS'}}cydriver.CUpointer_attribute_enum.CU_POINTER_ATTRIBUTE_P2P_TOKENS,{{endif}}):
                 if self._is_getter:
    -                self._token = _driver.CUDA_POINTER_ATTRIBUTE_P2P_TOKENS()
    +                self._token = _driver().CUDA_POINTER_ATTRIBUTE_P2P_TOKENS()
                     self._cptr = <void*><void_ptr>self._token.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -284,7 +291,7 @@ cdef class _HelperCUpointer_attribute:
                 self._cptr = <void*>&self._size
             elif self._attr in ({{if 'CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE'}}cydriver.CUpointer_attribute_enum.CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE,{{endif}}):
                 if self._is_getter:
    -                self._mempool = _driver.CUmemoryPool()
    +                self._mempool = _driver().CUmemoryPool()
                     self._cptr = <void*><void_ptr>self._mempool.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -340,7 +347,7 @@ cdef class _HelperCUgraphMem_attribute:
                               {{if 'CU_GRAPH_MEM_ATTR_RESERVED_MEM_CURRENT' in found_values}}cydriver.CUgraphMem_attribute_enum.CU_GRAPH_MEM_ATTR_RESERVED_MEM_CURRENT,{{endif}}
                               {{if 'CU_GRAPH_MEM_ATTR_RESERVED_MEM_HIGH' in found_values}}cydriver.CUgraphMem_attribute_enum.CU_GRAPH_MEM_ATTR_RESERVED_MEM_HIGH,{{endif}}):
                 if self._is_getter:
    -                self._cuuint64_t_val = _driver.cuuint64_t()
    +                self._cuuint64_t_val = _driver().cuuint64_t()
                     self._cptr = <void*><void_ptr>self._cuuint64_t_val.getPtr()
                 else:
                     self._cptr = <void*><void_ptr>init_value.getPtr()
    @@ -553,7 +560,7 @@ cdef class _HelperCUmemAllocationHandleT
             {{endif}}
             {{if 'CU_MEM_HANDLE_TYPE_FABRIC' in found_values}}
             elif self._type in (cydriver.CUmemAllocationHandleType_enum.CU_MEM_HANDLE_TYPE_FABRIC,):
    -            self._mem_fabric_handle = _driver.CUmemFabricHandle()
    +            self._mem_fabric_handle = _driver().CUmemFabricHandle()
                 self._cptr = <void*><void_ptr>self._mem_fabric_handle.getPtr()
             {{endif}}
             else:

    Would this be something we can add upstream as well? I can't reproduce this latest issue with the repro I had above though.

    I dug a bit deeper into why we're having these issues in the first place, and it looks like our Python launcher uses a custom module import machinery to allow us to do a few optimizations related to our internal setup. However, this machinery is simpler than the default importlib implementation and doesn't have special handling that it has.

    Specifically, IIUC importlib will do asynchronous module imports, and add modules to sys.modules early, before actually executing the module code (see https://github.com/python/cpython/blob/a68efdf09cfd81c64973d346ed34057f20172543/Lib/importlib/_bootstrap.py#L931)
    Then, if it encounters a module that is already initializing (e.g., due to cyclic, or self-import), it will block until the code importing the module finished executing (see https://github.com/python/cpython/blob/a68efdf09cfd81c64973d346ed34057f20172543/Lib/importlib/_bootstrap.py#L1377).

    Our implementation is completely missing this handling, which I think causes the error.

    However, I was still unable to reproduce the latest error in OSS. I tried something like the following, which still succeeded, so I might still be missing something:

    import importlib._bootstrap
    import sys
    
    orig_find_and_load = importlib._bootstrap._find_and_load
    
    
    def find_and_load(name, import_):
      print(f'find_and_load: {name}')
      if getattr(sys.modules.get(name, None), '_initializing', False):
        raise ModuleNotFoundError(f'Module {name} is already being initialized')
      return orig_find_and_load(name, import_)
    
    
    importlib._bootstrap._find_and_load = find_and_load
    
    
    from cuda.bindings import driver
    dir(driver)
  13. moved this from Done to Needs Triage in CCCLon Sep 16, 2025
  14. kkraus14 commented on Sep 16, 2025

    @kkraus14
    Collaborator

    Reopening the issue based on #789 (comment) and we can close once we've landed the change for utils.pxi upstream

  15. added a commit that references this issue on Sep 16, 2025
    4f77352
  16. mdboom commented on Sep 16, 2025

    @mdboom
    Contributor

    @gflegar: Thanks for the report. Your patch makes sense, but I worry that it would add unnecessary additional cache lookups on the critical path. Would you mind trying by PR here to see if that also resolves the issue in your environment?

  17. moved this from Needs Triage to Done in CCCLon Sep 17, 2025
  18. added a commit that references this issue on Sep 17, 2025
    d8b4acc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P0High priority - Must do!cuda.bindingsEverything related to the cuda.bindings moduleenhancementAny code-related improvements

Type

No type

Projects

  • Status
    Done

Relationships

None yet

Development

No branches or pull requests

Issue actions