Skip to content

fix(windows): load CUDA backend before device discovery - #1542

Open
IcarusAegis wants to merge 3 commits into
JustVugg:devfrom
IcarusAegis:fix/windows-cuda-device-discovery
Open

IcarusAegis wants to merge 3 commits into
JustVugg:devfrom
IcarusAegis:fix/windows-cuda-device-discovery

Conversation

@IcarusAegis

Copy link
Copy Markdown

I discovered this issue while building and testing Colibri's native Windows CUDA version (CUDA_DLL=1) to run Qwen3.8-Next on the GPU. This build uses MinGW-w64 GCC for the host executable and NVCC with MSVC for coli_cuda.dll, which the host loads at runtime. During testing of this native Windows build, automatic device selection fell back to CPU instead of using the available GPU.

When neither COLI_GPUS nor COLI_GPU is set, the Qwen tier queries coli_cuda_available_device_count() before calling coli_cuda_init(). The Windows loader returns zero at that point because the backend DLL has not been loaded, so automatic device selection falls back to CPU even with a working CUDA backend.

Load the backend on the discovery call. Keep discovery separate from coli_cuda_device_count(), which counts initialized contexts. If an older DLL lacks the discovery export, report that it needs rebuilding or an explicit COLI_GPUS selection; explicit initialization remains supported.

This follows #1537 , which added the missing wrapper but left the pre-initialization loading issue. Related: #1533 .

Environment

Component Details
OS Windows 11 Pro, 25H2
CPU AMD Ryzen 7 9800X3D
System memory 64 GiB
GPU RTX 4070 Ti SUPER, 16 GB
NVIDIA driver 591.86
Host compiler MSYS2 UCRT64 MinGW-w64 GCC 16.1.0, Rev5
CUDA toolkit / compiler CUDA 12.6; NVCC V12.6.20
Installed MSVC toolset v143 14.44.35207

Validation

  • Added three native Windows regression tests using a generated stub DLL; no GPU or CUDA SDK required. All three pass: discovery before init, missing discovery export with explicit init, and missing DLL.
  • Against the unmodified dev baseline (de26d3c), the discovery and missing-export diagnostic tests fail as expected.
  • Built the complete CUDA-enabled qwen38.exe with MinGW GCC 16.1.0 and freshly rebuilt coli_cuda.dll with NVCC 12.6.20 / MSVC 19.44.35228; both build logs contain no warnings or errors.
  • Real GPU smoke test with the newly built DLL on an RTX 4070 Ti SUPER: visible device count is 1 before init; initialized count remains 0 until init, then becomes 1.
  • git diff --check passes.
  • Full make -C c -j4 check passed with PYTHONUTF8=1: portable CPU build and C tests succeeded; Python reported 989 tests, OK with 100 environment-dependent skips. Without UTF-8 mode, the first run had nine GBK decoding errors. The test launcher used Windows SetErrorMode to prevent intentional malformed-DLL fixtures from opening interactive error dialogs.
  • make -C c cuda-test CUDA_ARCH=native passed on Windows. It also passed with NVCCFLAGS='-O3 -std=c++17 -ftz=false -arch=native -Xcompiler=-W3,-utf-8', which removes C4819 encoding warnings. The CUDA test log still contains warnings about an unused unsetenv function and ignored OpenMP pragmas in the CPU reference build.

@JustVugg

Copy link
Copy Markdown
Owner

Reviewed together with #1579 and #1580, full comparison in #1577 (comment).

Your diagnosis is the part the other two missed: device_count reports initialised contexts, not visible devices, so the fallback it replaces could only ever return 0 before init, which is exactly the silent path the issue describes. Replacing it with a named-symbol message is the right call and it should carry your name.

What #1579 has over this branch is the test, 148 lines against 78, and a more careful update to the HIP threading note now that the caller count changed. I have asked there to adopt your behaviour. If that lands you will see your reasoning in the merged comment; if you would rather bring the test across here instead, say so and I will take this one.

@JustVugg

Copy link
Copy Markdown
Owner

One thing to flag so my earlier comment is not read against a red tick: the CI on this branch shows a failure, but it is a run from two days ago whose job records no longer resolve, so there is nothing in it to read. I have re-run it rather than guess. If it comes back red on its own merits I will post what broke here.

Nothing in the review changes either way. The point about device_count reporting initialised contexts is a property of the code, not of the run.

Adapt DLL module-state probes and the HIP refusal control from ZhiyangK's PR JustVugg#1579. Preserve initialized-context semantics, verify missing-export diagnostics and explicit initialization, and distinguish a valid zero-device response. Clarify the three loader callers and their startup ordering.
@IcarusAegis
IcarusAegis force-pushed the fix/windows-cuda-device-discovery branch from 76feab0 to 7f14d6b Compare September 17, 2026 07:57
@IcarusAegis

Copy link
Copy Markdown
Author

Reviewed together with #1579 and #1580, full comparison in #1577 (comment).

Your diagnosis is the part the other two missed: device_count reports initialised contexts, not visible devices, so the fallback it replaces could only ever return 0 before init, which is exactly the silent path the issue describes. Replacing it with a named-symbol message is the right call and it should carry your name.

What #1579 has over this branch is the test, 148 lines against 78, and a more careful update to the HIP threading note now that the caller count changed. I have asked there to adopt your behaviour. If that lands you will see your reasoning in the merged comment; if you would rather bring the test across here instead, say so and I will take this one.

Thanks @JustVugg for the detailed review. I’d like to continue with #1542 and bring in the tests and threading clarification from #1579, adapting the tests to preserve the distinction between visible devices and initialized contexts, and to verify the missing-export diagnostic while keeping explicit initialization supported.
Thanks @ZhiyangK for the thorough regression tests, especially the DLL load-state checks and the HIP refusal coverage.

@IcarusAegis

Copy link
Copy Markdown
Author

Building on @ZhiyangK’s tests in #1579, I added more tests coverage for:

  • Discovery versus initialization: a stateful backend fixture verifies that discovery does not initialize devices, and that visible-device counts remain distinct from initialized-context counts.
  • Explicit initialization: nonzero device ordinals and device-list ordering, plus direct initialization without a discovery call, including older DLLs without the optional discovery export.
  • Failure handling and lifecycle: zero visible devices, initialization failure, missing required exports, repeated discovery, one-shot load-failure behavior, and shutdown/runtime-reference ownership for CUDA and HIP.
  • Actual tier startup paths: qt_init and qt_init_fp8, covering automatic/explicit selection, empty-variable fallback, COLI_GPUS/COLI_GPU precedence, COLI_PLACE device merging and deduplication, and ensuring failed initialization does not start the uploader.

Validation

  • 105 focused tests passed with no skips: the existing 78 loader tests, 13 discovery tests, and 14 tier-selection tests, with additional parameterized scenarios.
  • Full make check passed: the C test gate passed; Python reported 1,013 tests, with 913 passed and 100 skipped.
    All 7 deliberately introduced regressions were caught, including restoring the original pre-init check, falling back to initialized-context counts, initializing during discovery, and leaking a DLL reference.
  • GPU validation on an 4070 Ti SUPER: all 9 CUDA test programs and 16 real DLL/tier scenarios passed using a freshly rebuilt backend. Discovery reported one visible device before initialization while the initialized-context count remained zero, then became one after initialization. The probe’s GPU matrix results matched the CPU reference with zero maximum absolute error. Device masking, invalid-device rejection, automatic/explicit selection, and FP8 startup also passed.

Reviewed together with #1579 and #1580, full comparison in #1577 (comment).

Your diagnosis is the part the other two missed: device_count reports initialised contexts, not visible devices, so the fallback it replaces could only ever return 0 before init, which is exactly the silent path the issue describes. Replacing it with a named-symbol message is the right call and it should carry your name.

What #1579 has over this branch is the test, 148 lines against 78, and a more careful update to the HIP threading note now that the caller count changed. I have asked there to adopt your behaviour. If that lands you will see your reasoning in the merged comment; if you would rather bring the test across here instead, say so and I will take this one.

Extend the discovery fixture with separate visible-device and initialized-context state. Cover CUDA/HIP discovery, explicit device forwarding, old DLL diagnostics, failure cleanup, runtime ownership, and repeated calls without changing the original direct-init tests.

Exercise real qt_init and qt_init_fp8 selection paths with a stateful backend and add both suites to the mandatory Windows CI job. Adapt module-state and HIP control ideas from ZhiyangK's JustVugg#1579.

Validation: 105 focused tests passed without skips; make check passed (Python: 913 passed, 100 skipped); seven regression mutations detected. On RTX 4070 Ti SUPER, nine CUDA test programs and sixteen real DLL/tier scenarios passed. Multi-GPU, AMD HIP hardware, and full-model inference remain unverified.

Co-authored-by: ZhiyangK <ZhiyangK@users.noreply.github.com>
Edo771977 added a commit to Edo771977/colibri that referenced this pull request Sep 17, 2026
…'s one-line diagnostic

The conflict resolution in 0c6cb27 kept the fork's JustVugg#1542 body (return 0 when
the DLL lacks coli_cuda_available_device_count) over dev's fallback. That
contradicts tests/test_backend_loader.py's
test_the_probe_falls_back_for_a_dll_without_the_export, which arrived with the
dev merge and fails on the Windows loader job.

The fallback is upstream's contract, so it stays; what the fork keeps is the
stderr line naming the stale DLL, printed once. Before init device_count() is
0, so tests/test_cuda_loader_discovery.py's expectations are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@IcarusAegis

Copy link
Copy Markdown
Author

Hi @JustVugg, could you clarify how you’d like me to proceed with #1542?
Following your earlier review, I replied that I’d continue with this PR and incorporate @ZhiyangK’s tests from #1579. I then pushed the expanded coverage.
I noticed #1579 has since been merged, and #1542 now has conflicts. Would you like me to build on the changes merged from #1579 and complete the remaining work in #1542? I’m happy to update this PR against the latest dev version. Thanks!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants