ci: give the owned-CUDA worker a Windows build gate that can reach nvcc - #10
Conversation
The engine-cuda build script hard-wired bin/nvcc (no .exe, so cc-rs fails on MSVC) and forwarded -Xcompiler=-fPIC to the host toolchain (unknown option out of cl, fatal on Windows). Link search now covers CUDA 13's bin/x64 layout where the cublas import library sits beside the DLL. Adds windows-owned-cuda-manual: assembles a SHA-pinned CUDA 13.2.1 toolkit from NVIDIA redist archives (no installer, no driver), builds ck-synapse-worker-cuda with --features cuda, and refuses a hollow-green gate by proving the exe imports the CUDA DLLs off-PATH and starts on-PATH. Widens the lint set to the two owned-CUDA crates, which had never been clippy-gated and fail --all-targets -D warnings without the cfg(unix) handshake helpers gated.
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…DA-13 DLL search path
Addresses the cubic-dev review on the owned-CUDA gate:
- The build step never initialised the MSVC developer environment, so nvcc
failed to find cl.exe before compiling anything. Reproduced exactly the
same way locally: 'nvcc fatal : Cannot find compiler cl.exe in PATH'.
The gate now enters the dev shell via vswhere + Enter-VsDevShell for that
step only.
- Expand-Archive yields one nested package directory per archive, so the
merge copied the wrapper instead of its contents; bin/nvcc.exe never
existed at the toolkit root and every dispatch died on the guard. Copy
the contents now ('*\*').
- Local run-proof found a third defect the review missed: CUDA 13 keeps
its runtime DLLs in bin/x64, not bin. Both directories are exported and
restored in the import probe, which without this fails its on-PATH half
with the same 0xC0000135 the off-PATH half expects.
Local end-to-end proof (upgrades the "not verified locally" caveat)
The author machine turned out to have everything needed to run this lane by hand — RTX 4050 Laptop (driver 610.88, CC 8.9), MSVC 2022 BuildTools, and the CUDA 13 runtime DLLs already on disk from a llama.cpp deployment. So the exact recipe this gate automates was executed locally, and the full loop works: 1. Redist assembly — the six pinned archives (nvcc/crt/nvvm/cccl/cudart 13.2.x, libcublas 13.4.0.1) downloaded, SHA-256-verified against the pins in this PR, merged into one root the same way the gate does. 2. Build — This also confirmed, by reproduction, the two P1s fixed in 465072c: without the dev shell nvcc dies with 3. Import probe — the same off-PATH/on-PATH discriminator the gate asserts, run against the locally built exe: The sidecar row is direct evidence for blocker #1 in the description: the worker runs from co-located 4. Real work, not just The model runs: Qwen3-0.6B f16, CUDA graphs captured per shape ( Throughput against the llama.cpp CUDA lane (Q8_0, same GPU,
The owned lane is ~3.4× faster at the large sustained shape while carrying f16 instead of Q8_0, and it runs ~95% GPU-bound (646.6 measured vs 683 GPU floor). At 256 the measured 459 vs 667 floor is the one-time capture inside a 5-round window, not steady state. Transports differ (named pipe vs HTTP); the per-item small-batch path (~16 items) is unmeasured. Three observations for whoever picks up the distribution follow-ups:
Fixes from the review round are in 465072c (vcvars dev shell, archive flatten, |
|
Reviewed, and I verified each claim against CI is red on one thing, and it is mine rather than yours:
"windows-owned-cuda-manual": (
"github.event_name == 'workflow_dispatch'",
"The owned-CUDA Windows build is an explicit manual gate, not a push-triggered train gate.",
),Worth knowing about the run: Windows passed, including the widened clippy set — that is the real confirmation of your Two smaller notes, neither blocking:
I will re-run CI once the allow-list entry is in. |
check-train-preconditions.sh keeps an explicit allow-list of dispatch-only jobs; a path-dependent 'if' outside it fails the precondition so a new manual gate has to be registered on purpose. windows-owned-cuda-manual is deliberately dispatch-only like the two llama GPU gates, so add its entry (clears the red linux precondition step reported on the PR). Also record the delay-load caveat on the import-probe step: the 0xC0000135 discriminator depends on cudart/cublas staying load-time imports; a future delay-load build turns the probe into a false failure that names its real cause rather than hiding it.
|
Allow-list entry is in (eb79421), plus the delay-load caveat comment on the probe step, worded as you suggested. The precondition script passes locally against the current tests.yml with the new job registered (rc=0). Re the caching note — agreed, left as-is for a manual lane; the entry point when it matters is obvious. Two corrections to my previous comment, since I'd posted it before finishing the measurement work:
Both are reflected in the description's Verification section. |
|
Verified end to end and merged. The one thing neither you nor PR CI could exercise from a fork is the gate itself, since
Coverage on the rest:
On your throughput correction — the self-catch is appreciated, and the corrected figure cross-checks against measurements you had no way to see. Our own owned-CUDA Qwen3-Embedding-0.6B run on an RTX 4090 recorded 63,330 tok/s, against llama.cpp CUDA f16 GGUF at 9,205 on the same box. Your 646.6 items/s at ~24 tokens/item is ~15,500 tok/s on a laptop 4050, which is close to the 4090 figure scaled by hardware class. The ratio gap (your 3.4x against our 6.9x) is explained by the baseline: you measured llama.cpp at Q8_0 where we measured f16 GGUF, and quantized formats do relatively better on a bandwidth-bound engine — the same effect we saw on RDNA3, where Q6_K beat f16. So your number is independent corroboration on a third GPU we had never measured, and I would not have had that without the comment. One unrelated finding the dispatch surfaced, recorded so it is not mistaken for yours: Thanks for this. The gap it closes is real: two crates that nothing compiled on any platform, which is how |
Problem
synapse-worker-cuda— the owned-CUDA, VRAM-resident embedding worker — has never had a Windows build gate, and its build script cannot reachnvccon MSVC.crates/synapse-engine-cuda/build.rshard-wired the compiler path asbin/nvcc. On Windows the binary isnvcc.exe;cc::Buildreports the missing path as a failed tool rather than falling back, so every--features cudabuild on Windows dies before compiling a single kernel. Two follow-on defects sit behind that one:-Xcompiler=-fPICis forwarded unconditionally. Position-independent host code is an ELF concern; MSVC'sclrejects the unknown option and nvcc surfaces it as a build failure.lib/x64. CUDA 13's redist archives placecublas.libbeside its DLL inbin/x64(the 12.x layout keeps it underlib/x64), so a 13.x toolkit does not resolve-lcublas.Neither crate is in
SYNAPSE_CRATES, so nothing in CI compiles or lints them on any platform. That gap is not theoretical:synapse-worker-cudafailscargo clippy --all-targets -- -D warningson Windows today, because the Unix handshake helpers (read_json_frame,validate_ack,WorkerHelloAck) are dead code in thecfg(windows)build. It has never been caught because it has never been gated.Change
crates/synapse-engine-cuda/build.rs— resolvenvcc.exeon Windows (CUDACXXstill wins when set); gate-fPICto non-Windows hosts; searchbin/x64andbinin addition tolib/x64on Windows. The PTX distribution contract is unchanged: virtual archcompute_75only, nosm_*SASS image. (Turing/7.5 remains CUDA 13's lowest supported arch — 13.0 removed pre-7.5 offline compilation, not 7.5 itself.)crates/synapse-worker-cuda/src/main.rs—cfg(unix)on the Unix-only handshake imports and helper;bail!fully qualified so the import is no longer Windows-dead. Behaviour is unchanged on every platform; the change is cfg-correctness only..github/workflows/tests.yml— addwindows-owned-cuda-manual, mirroring the structure oflinux-llama-cuda-manualandwindows-llama-vulkan-manual, and widenSYNAPSE_CRATESwith the two owned-CUDA crates so the fmt/clippy gates start covering them.Toolkit assembly
Hosted Windows runners have no nvcc, and the repository's existing Linux lane cannot use apt on Windows. The gate assembles a toolkit from NVIDIA's own per-component redist archives — pinned version + SHA-256, no installer, no driver, nothing written outside
$RUNNER_TEMP:cuda_nvcccuda_crtlibnvvmcuda_ccclcuda_cudartlibcublasSix components are what the kernel sources actually need:
cuda_family_common.cuhincludescublasLt.h,cuda_fp16.h,cuda_runtime.h, andcrt/math_functions.hpulls the cccl/crt header chains;nvccneedscicc/ptxas(nvcc + libnvvm) andcrtheaders; the link flags namecuda/cublasLt/cublas/cudart.The build step enters the MSVC developer environment first (vswhere →
Enter-VsDevShell): nvcc drivescl.exeby name, and it is not onPATHon a clean runner — reproduced locally asnvcc fatal : Cannot find compiler 'cl.exe' in PATH. CUDA 13's redist archives also keep the runtime DLLs inbin/x64rather thanbin, so both directories are exported.CUDA 13, deliberately. 13.x is the line whose runtime sonames (
cudart64_13.dll,cublas64_13.dll,cublasLt64_13.dll) llama.cpp's Windows CUDA builds already ship — so this worker drops into an existing CUDA-13 deployment instead of dragging a second, conflicting runtime beside it. The engine's declared floor (driver API ≥ 12040) is satisfied: a 13.x-built binary runs against any 580-or-newer driver.Anti-hollow-green assertion
Building is not the same as building with the backend compiled in.
--features cudais the only thing that linkscudart/cublas, and on Windows those are load-time imports: the exe cannot start when they are absent fromPATH(STATUS_DLL_NOT_FOUND,0xC0000135) even though its--versionpath never calls into them. The gate exploits that as a dependency-free discriminator:0xC0000135⇒ the CUDA backend is genuinely baked in;A worker that silently compiled without CUDA prints
--versionin both cases, and the step refuses that. Evidence files follow the existingnonmac-build-gates-v1contract (execution_status, binary SHA-256,skipped_is_not_pass=true,manual_gate=mandatory); the produced exe ships as a CI artifact.Verification
Windows x64, MSVC 2022 Build Tools, redist-assembled CUDA toolkit (same archive set and digests as the gate):
cargo fmt --all --check→ clean.cargo clippy -p synapse-engine-cuda -p synapse-worker-cuda --all-targets -- -D warnings→ clean (fails before this change: 3 dead-code/unused-import errors).cargo test -p synapse-engine-cuda -p synapse-worker-cuda→ 7 + 3 pass, 0 fail.test,linux-llama-cuda-manual,windows-llama-vulkan-manual,windows-owned-cuda-manual.cuda_cudart12.6.68 and 13.2.75 Windows archives downloaded and SHA-256-verified against NVIDIA's manifest;libcublas/libnvvm/cuda_crt13.2.1 layouts confirmed from their central directories by ranged reads (contents quoted above:bin/x64/cublas64_13.dll,lib/x64/cublas.lib,nvvm/bin/cicc.exe,include/crt/math_functions.h).Not verified locally— superseded: the--features cudacompile and runtime probe were since executed end-to-end on a local Windows box (RTX 4050, driver 610.88, MSVC BuildTools) with the exact recipe this gate automates. See the "Local end-to-end proof" comment on this PR for the build log, the off-PATH/on-PATH import-probe results, and a real Qwen3-Embedding-0.6B safetensors load + embed round trip. The first hosted dispatch still re-checks the redist pinning independently.Steady-state from that run (per-shape wall timing; item counts reconstructed from the engine's own shape logs): owned-CUDA f16 at 1024 × ~24-token items = 646.6 items/s sustained, ~95% of the 683/s stage-timer GPU floor, vs llama.cpp CUDA Q8_0 (same GPU, HTTP endpoint) at 188 items/s at the matched 1024-item shape — ~3.4× faster while retaining f16. VRAM is stated as a delta over this box's ~2 GiB system baseline: +75 MiB persistent weights at load, +2,665 MiB at the 1024-batch steady state (two runs: +2,650 / +2,665). Cold load 26–41 s, dominated by the 1.19 GB sha256 verify plus BF16→F32 host conversion. Method, caveats (transport differs; the 5-round 256× window includes one-time graph capture), and raw logs are in the proof comment.
Deliberately not in this PR: release assets, installer inventory, config auto-wiring. The
testlane's widened clippy set is the only push-triggered change; everything CUDA isworkflow_dispatch-gated.Why the release asset and
ck setup synapseinstall are deliberately excludedThe tempting follow-up — add
ck-synapse-worker-cudato thewindows-x64release matrix and to Subc'scomponent_binaries_for_target— would publish a worker that cannot run, and break installs for most Windows users. Three independent blockers, each verified in source at 5680bd7 / 0ed4dcb5:build.rslinkscudart/cublas/cublasLtdynamically; onlynvcuda.dllships with the driver. Subc'sReleaseArtifactSource::installcopies only the named candidate out of each zip, so packaged DLLs are discarded. Worse,Acceptance::RunsAndReportsexecutes<destination> --versionbefore writing configuration — and a PE with unresolved load-time imports fails before executing a single instruction. The install would refuse, aborting the wholeck setup synapsecomponent on every Windows machine without the toolkit. Sidecar-file delivery needs an installer-contract change insubconscious, designed together with a per-target asset split (the shared non-Darwin match arm currently would also make Linux and Windows-Arm64 read release-incomplete).load_worker_backend_blockingrequires the per-model preloadworker_binorSYNAPSE_OWNED_CUDA_WORKER_BIN; installing the exe besideck-synapse.exedoes not make the module use it. A default needs either a Subc config-writer emit (modules.synapse.env, precedent: claustrum'sCK_MASTER_KEY_PATH) or a new sibling-of-exe convention — which sits on the same code path as the llama worker and must stay gated to owned-CUDA.ensure_owned_cuda_floor()readsSYNAPSE_CUDA_DRIVER_API/CUDA_DRIVER_APIandSYNAPSE_CUDA_COMPUTE_CAPABILITY/CUDA_COMPUTE_CAPABILITYand returnsHardwareUnavailablewhen either is absent — refusing before the worker spawns. There is no probe, CI step, or installer that sets them on any platform. The engine already has the primitives to derive both (cuInit+ device attributes,device_meets_floor); wiring a real probe into the module's load path is the honest fix, and it is its own change.Landing the asset + inventory pair without those would be a hollow-green release: an artifact that passes
--versionacceptance on some machines and hard-fails the installer on the rest. This PR delivers the gate that makes the binary buildable and provably CUDA-backed; the distribution shape should be designed as a unit against the three facts above.