fix: ask torch, not the redirected probe, before aliasing cuda to flagos - #419
Conversation
8cffc6f to
2ed6119
Compare
Branch rebased and re-measured on
|
db4a578 to
5112740
Compare
`_alias_cuda_to_flagos()` decided whether to install by asking `torch.cuda.is_available()`. That probe has two writers in this process and torch_fl is one of them: importing `flag_gems` during `_phase_vendor_compat()` reaches `flagos._lazy_init()` and then `torch_fl.compile`, whose module-level `_patch_native_cuda_probe()` repoints the probe at `torch.flagos.is_available` and saves the function it replaced as `torch.cuda._flagos_original_is_available`. That happens a whole phase before `_phase_ecosystem()` calls the alias, so the guard read the redirect, saw `True` on a build with no CUDA runtime, returned early, and left the six `torch.cuda.*` entries the alias owns as stock CPU torch's with `_cuda_alias_active` `False`. Dynamo's `cuda_extra_check` then called `torch.cuda.current_device()` and raised `AssertionError: Torch not compiled with CUDA enabled`, so `torch.compile(model, fullgraph=True)` on a `flagos` module could not compile -- issue flagos-ai#264. The saved original was written and read by nothing. `_real_cuda_is_available()` gives it a reader, falling back to the live probe when nothing redirected it, and the guard calls that. Measured on MUSA MTT S5000, both arms on upstream `f84eb66`: `_cuda_alias_active` False -> True; the four aliased `torch.cuda.*` entries False -> True; `torch.device('cuda')` `device(type='cuda')` -> `device(type='flagos')`; `torch.zeros(2).cuda()` and `torch.cuda.current_device()` `AssertionError` -> ok; flagos-ai#264's own reproducer `AssertionError` -> `OK (1, 10)`. No conf, kernel or route changes: the full 467-overload MUSA survey reports the same summary with no verdict difference, and the alias stays off under `FLAGOS_ALIAS_CUDA=0`. Tested: pytest tests/unit/test_cuda_alias_guard.py (6 failed pre-fix, 6 passed after); pytest tests/unit -q -> 9 failed, 780 passed, 109 skipped against a 15 failed, 774 passed, 109 skipped pre-fix baseline, the two differing by exactly the new file's six tests over the same 897 collected. Fixes flagos-ai#264 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5112740 to
eff8a95
Compare
Both reference documents carry claims that stopped being true when the issues behind them closed, and neither had a record of what replaced them. `docs/reference/hf-coverage.md` lists five tracked causes from the 2026-09-02 MUSA baseline and still presents all five as open work. Four are closed: #250/#265 by PR #280 and #282, #262/#268 by PR #398, #266 by PR #278, #264 by PR #419. Only #263 is still open. A new "What has changed since this baseline" section records each closure with the issue and the commit that landed, and states what was actually re-measured: the eight SDPA nodeids #268 borrowed, with `{"PASS": 8}` under the harness's device shims and an `allclose` residual without them that belongs with #248. The baseline's own `Affected tests` counts are deliberately left alone — they are dated measurements of `64e60dd`, and rewriting them against a later tree would stop them matching the run they came from, which is what the dedup parser reads them for. `docs/reference/operator-support.md` records the 2026-08-30 cohort as excluding "the known float64 `mm` gap". PR #275 moved `mm` and `mm.out` onto the FlagGems route on 2026-09-15, so that exclusion is historical; the later entries in the report count the op. A paragraph after the row says so and names what replaced the mudnn capability gap — a different defect on the route that now serves the op, the FlagGems fp64 tile exceeding the device's 192 KiB of shared memory past `M > 32` and `N > 32`, filed as #428. Documentation only; no code, no routing and no test changes. Verified: `ruff check .` -> All checks passed; `ruff format --check .` -> 311 files already formatted; `pytest tests/unit/test_transformers_automation.py -q` -> 61 passed; `pytest tests/unit/ -q` -> 6 failed, 761 passed, 131 skipped, and the same six fail identically with these edits stashed, so they pre-exist on d5f82ae. `transformers_deduplicate.extract_baseline_fingerprints` still reads the edited coverage doc without error. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
AI Agent Information
torch.compileunusable on MUSA). Measuring the repro on MTT S5000 traced it to_alias_cuda_to_flagos()'s guard reading the CUDA probe that_patch_native_cuda_probe()had already redirected at torch_fl's own accelerator.Summary
_alias_cuda_to_flagos()decided whether to install by askingtorch.cuda.is_available(). On a build with no CUDA runtime that probe is nolonger torch's answer --
torch_fl.compile.inductor_backend._patch_native_cuda_probe()repoints it at
torch.flagos.is_availableso Inductor's CUDA-shaped FakeTensorprobe finds the accelerator, and that import runs before the alias does. The guard
therefore read "yes, there is CUDA" on exactly the builds the alias exists for,
returned early, and left
torch.cuda.current_device,synchronize,device_countandget_device_propertiesas stock CPU torch's. Dynamo'scuda_extra_checkcallstorch.cuda.current_device()while deciding whether tocompile for CUDA, so
torch.compile(model, fullgraph=True)on aflagosmoduledied with
AssertionError: Torch not compiled with CUDA enabled-- #264.The fix adds
torch_fl._real_cuda_is_available(), which reads the function_patch_native_cuda_probe()saved astorch.cuda._flagos_original_is_available(falling back to
torch.cuda.is_availablewhen nothing redirected it), and callsit from the guard. On MUSA the alias now installs,
device="cuda"resolves to theflagosdevice, andtorch.compilecompiles. No conf, route or kernel changes:where the alias now runs it hands out the same
flagosdeviceFLAGOS_BACKEND_CONFIGalready routed.Change Type
Platforms Affected
Problem Analysis
What was broken/missing?
On MUSA,
import torch_flleft the process in a half-shimmed state: the sixtorch.cuda.*entries that_alias_cuda_to_flagos()owns were still stock CPUtorch's, so every
"cuda"-shaped call raised, whiletorch.cuda.is_available()reported
True.torch.compile(model, fullgraph=True)could not compile, andtests/manual/flaggems_overload_survey.pycould not run its child unmodified(recorded as an evidence gap in
docs/reference/operator-support.md).Why did it happen?
Two torch_fl functions write
torch.cuda.is_available, in this order:_phase_vendor_compat()->_patch_flaggems_philox()->from flag_gems.utils import random_utils->import flag_gems, whosefused/FLAkernels calltorch.flagos.current_device()at module scope ->flagos._lazy_init()->torch_fl.compile.flagtree_shim->torch_fl.compile.inductor_backend, whosemodule-level
_patch_native_cuda_probe()setstorch.cuda.is_available = torch.flagos.is_availableand saves the replacedfunction as
torch.cuda._flagos_original_is_available;_phase_ecosystem()->_alias_cuda_to_flagos(), whose guard readtorch.cuda.is_available().The guard was written to mean "is there real CUDA here". After (1) it no longer
answers that question, and it answers "yes" on precisely the CUDA-less vendor
builds the alias exists to serve. The saved original was written and read by
nothing, which is the tell: the intent was recorded, the read was missing.
Investigation process:
import torch_flalone:torch.compile(m, fullgraph=True)raisedAssertionError: Torch not compiled with CUDA enabled.cuda_extra_check->torch.cuda.current_device()->torch.cuda._lazy_init.import torch_fland foundtorch.cuda.is_available() == Truewith all four aliased entries unchanged andtorch_fl._cuda_alias_active == False.__import__hook:flag_gemsisimported by
_phase_vendor_compat()'s_patch_flaggems_philox(), and its FLAkernels reach
flagos._lazy_init()->torch_fl.compile-> the patch, so theredirect is in place a whole phase before
_phase_ecosystem()calls the alias.torch.cuda._flagos_original_is_availablereturnsFalseon MUSA, i.e. it is the answer the guard wanted.
tests/unit/test_cuda_alias_guard.pyand ran it against the pre-fix tree(
git show HEAD:...) to prove it catches the defect: 6 failed. Post-fix: 6passed.
Solution Design
Implementation approach:
torch_fl/__init__.pygains a small helper next to the alias, and the guard callsit:
Key design decisions:
and only one of them is "torch's opinion". The saved copy is the only thing in
the process that still answers "does torch have a CUDA runtime", and it already
existed -- this change gives it a reader.
torch.cuda.is_available. When nothing redirected it (nonative accelerator, or no flagos device), the live probe is torch's answer, so
the guard keeps its old behaviour there.
FLAGOS_ALIAS_CUDA=0opt-out, theTorchFunctionMode, thetorch.devicewrapper and
_cuda_alias_activeare untouched.the guard with
astand fails if it readstorch.cuda.is_available()again,because an outcome-only test on a CUDA host cannot see the difference.
Code changes by file:
torch_fl/__init__.py: new_real_cuda_is_available()(docstring records themeasured failure mode);
_alias_cuda_to_flagos()'s docstring paragraph on whenit is a no-op now distinguishes "torch reports CUDA" from "the redirect says
so"; the guard reads the helper.
torch_fl/compile/inductor_backend.py: comments only --_patch_native_cuda_probe()now documents that the function it replaces is read back by
torch_fl._real_cuda_is_available, and the save site says so too.tests/unit/test_cuda_alias_guard.py(new): 6 tests over the helper'sprecedence, the guard's shape, and the child-process effect with
FLAGOS_ALIAS_CUDAset/unset.docs/reference/operator-support.md: new MUSA section with the measuredbefore/after, and a new
## Update Historyrow.Changes by commit:
5112740-fix: ask torch, not the redirected probe, before aliasing cuda to flagos-torch_fl/__init__.py,torch_fl/compile/inductor_backend.py,tests/unit/test_cuda_alias_guard.pyanddocs/reference/operator-support.md, the last two being the new test file and the operator-support record.The branch was rebased three times after this PR was opened -- onto
17b5d30,then onto
892432b, and now ontof84eb66, which is upstream's head; it had beenopened on
8cffc6fagainst877afb1. Each rebase resolved one conflict indocs/reference/operator-support.md's Update History table, because every upstreamlanding adds a row to it, and needed no code change. The single commit on the
branch was amended as its message and its docs provenance were corrected, and the
tip is
5112740. Upstream#410addscsrc/aten/sparse_csr_ops.ccand editscsrc/aten/device_boxing.h, and#416editscsrc/profiler/cupti_shim.h, so theextension was rebuilt from the rebased source with
FLAGOS_ACCELERATOR=musa FLAGOS_BUILD_VENDOR=1 FLAGOS_BUILD_FLAGGEMS=1 FLAGOS_BUILD_FLAGGEMS_CPP=0 python setup.py build_ext --inplace, leavingtorch_fl/lib/libtorch_fl.soat md5e34ada57822e793d42c3dd1c18b5c4f8-- the samevalue the previous base's rebuild produced, since the two newest upstream commits
reach
csrc/only undercsrc/aten/backends/gcu/, which this build does notcompile. Every measurement below was re-taken on that tree, with the pre-fix arm
being upstream
f84eb66withtorch_fl/__init__.pyat the pre-fix revision(
899f97fdf54c24936f4bf852d84dba29) and this PR's test file copied in so the unitbaseline collects it -- upstream's comment-only
inductor_backend.pyneeds norevert, so those are the only two differences between the arms.
Verification
Pre-submission Checklist
Linting Results
$ /publi-flash/lvyufeng/env/miniconda3/envs/musa_test/bin/ruff check --config pyproject.toml All checks passed! $ /publi-flash/lvyufeng/env/miniconda3/envs/musa_test/bin/ruff format --check --config pyproject.toml 311 files already formatted(
ruffis not installed in the py310-musa environment this was measured in; the0.15.12 binary above is used at the repository root with the project config.)
Test Results
--collect-onlylists 897 tests on this base against 886 on the previous one, andall eleven additions are upstream's -- nine in
#422's newtests/unit/test_nccl_extension_fallback.pyand two in#423'stests/unit/test_transformers_automation.py. Both arms here collect all 897, sothe comparison is unaffected; the runs' own totals are one higher than that count
because they also carry the module-level collection skip
tests/unit/bpu/test_qdq.pyreports, whoseonnximport this environment lacks.The failing sets differ by exactly the six tests in the new file: 774 + 6 = 780 and
15 - 6 = 9, so no other test moved in either direction, and the nine that remain
are the same nine the previous base's after arm failed, id for id. They are
pre-existing and unrelated: six come from
libentry._descriptor_cache_keybeingabsent in
/tmp/FlagGems/src/flag_gems/utils/libentry.py, and three fromtest_musa_rng_bridge.py, which pass in isolation and fail only whentest_ascend_platform_marker.pyruns first in the same session.tests/unit/bpu/test_device_alias.py, which also covers the alias, is 6 skippedon MUSA (its
conftest.pyskips unless the accelerator isbpu).Manual Verification
Both arms kept Triton's driver selection intact (
mthreads is_active=True,driver.active=MusaDriver), so the alias disturbs nothing in the compilerstack. Note that the
OSError: libcuda.so.1in the issue's report is notreproducible on this tree; see Related Work.
Breaking Changes
N/A --
Breaking Changeis not checked.Code Quality Verification
Style Consistency
Edge Cases Considered
_patch_native_cuda_probe()returns before redirecting (
torch.flagos.is_available()is false), so nosaved function exists, the helper falls back to the live probe, gets
False,and the alias installs -- unchanged from before.
helper reports the stock probe as
True, and the alias stays off. The boxingpath submits real CUDA work, which is why the alias must not hijack
cudathere.
FLAGOS_ALIAS_CUDA=0. Still honoured; asserted in the child-process test.torch.cuda.is_availablewriter. The helper readstorch.cuda._flagos_original_is_availableif present, so a redirect from anysource is seen, not just this one.
mutates state.
Potential Risks
but it is a behaviour change beyond the compile path: code that used to fail
loudly on
device="cuda"now silently lands onflagos, andtorch.deviceisno longer
torch._C.device. Measured on MUSA: the full unit suite and theFlagGems overload survey both show no new failure (see below);
FLAGOS_ALIAS_CUDA=0restores the old behaviour for anyone who needs the loud failure.
torch.cuda.is_availableat a second site without saving. The helper's fallbackis the live probe, so a second writer without a save would reproduce the old
bug rather than a new one, and the
asttest would not catch it.Rollback Plan
Revert the commit. The change is three lines of behaviour (
__init__.py) plus acomment-only edit and a new test file; no conf, kernel or route is involved, and
the pre-fix state is reachable without a revert by setting
FLAGOS_ALIAS_CUDA=0.Related Work
torch.compileon MUSA), whose own reproducer this change repairs.Its diagnosis does not hold up on this tree: the
OSError: libcuda.so.1itreports was taken at
2e64a8dand no longer reproduces, the reproducer nowdying earlier in dynamo with
AssertionError: Torch not compiled with CUDA enabled, and neither proposed fix (skip the test, or write a MUSA Tritonbackend) addresses that -- the measured cause is the guard above, and a comment
on the thread corrects it.
left to a follow-up: measured on the harness,
Qwen3ModelTest::test_generate_compile_model_forward_fullgraphfails before and after, but for different reasons --
AssertionError: Torch not compiled with CUDA enabledpre-fix, and post-fixRuntimeError: Could not find an active GPU backendfromtorch._inductor.runtime.triton_helpers.set_driver_to_gpu()in an Inductor compile worker. Triton's
mthreadsdriver reports inactive inthat worker because it is a fresh interpreter that neither imports
torch_flnor -- under the harness's
TORCH_DEVICE_BACKEND_AUTOLOAD=0-- autoloadstorch_musa, sohasattr(torch, "musa")is false there. The sibling testtest_generate_compilation_all_outputsdoes goFAIL->PASSwith this fixonce the harness shims are out of the way (
HF_TEST_NO_DEVICE_SHIMS=1).f84eb66after opening, so#410(compressedsparse on
SparseCsrPrivateUse1),#414(HF harness device shims),#415(bool neg),
#416(generated CUPTI cbid table),#422(report the missing_flagos_ncclextension instead of aNoneTypeAttributeError) and#423(GCU
new_onesfrom a generated kernel, plus the HF harness's nodeid repair)are now below it, as is the CI-only
#420. None of them interacts with theguard; all are reflected in the re-taken measurements below.
Explicitly Not Included
(
test_eager_matches_sdpa_inference_0{0..7}_fp16_*, fournan, four ~1e-5).Unchanged by this PR and still a separate open defect with no issue filed.
their operator-support rows are carried over unchanged and are not
revalidated.
torch_fl/compile/device_interface.py's two docstrings that attribute theprobe redirect to the alias rather than to
_patch_native_cuda_probe(). Theattribution is imprecise both before and after this change; touched only for
accuracy if the reviewer asks.
Human Review Notes
Areas needing special attention:
torch.cuda._flagos_original_is_available, a privateattribute written by another module -- is that the right coupling, or would you
rather
_patch_native_cuda_probe()expose a public predicate?the alias should be on for native accelerators, not just opted into.
meaningful; on a CPU-only CI host they reduce to the three in-process tests.
Questions for reviewer:
docs/reference/operator-support.mdrecord the survey runtime increase(the after-arm survey is slower, since every overload now syncs through
torch.flagos.synchronize), or is that out of scope for the report?attached to torch.compile Triton backend requires libcuda.so even on flagos device #264's thread?
Additional Context
Measured A/B, MUSA MTT S5000, one process per arm
torch_fl/__init__.pymd5899f97fdf54c24936f4bf852d84dba29(before) against9f941ef7f0899efa8dc73202b03348b4(after).import torch_fltorch_fl._cuda_alias_activeFalseTruetorch.cuda.current_device is torch.flagos.current_deviceFalseTruetorch.cuda.synchronize is torch.flagos.synchronizeFalseTruetorch.cuda.device_count is torch.flagos.device_countFalseTruetorch.cuda.get_device_properties is torch.flagos.get_device_propertiesFalseTruetorch.device is torch._C.deviceTrueFalsetorch.device('cuda')device(type='cuda')device(type='flagos')torch.cuda.current_device()AssertionError0torch.randn(2, 2, device='cuda')AssertionErrorflagos:0tensortorch.zeros(2).cuda()AssertionErrorflagos:0tensorAssertionErrorOK (1, 10)torch.cuda.is_available()TrueTruedriver.activeMusaDriverMusaDriverHarness A/B (`tests/manual/transformers_hf_tests.py --model qwen3 --offline
--pytest-arg=-k --pytest-arg=test_eager_matches_sdpa_inference`)
The
FAILid sets are byte-identical between the two arms, and the eight are thefp16 SDPA defect listed under "Explicitly Not Included". The default mode no longer
discriminates: #414, newly below this branch, gave the harness device shims that
make it pass on either arm, so
HF_TEST_NO_DEVICE_SHIMS=1is the mode that stillreaches the shimmed probe. Neither mode is where this change shows -- the
reproducer and the alias-state probe are.
Survey A/B (full MUSA cohort,
flaggems_overload_survey.pyv6)Conf SHA-256
87d150533c73e4ca40a24c2588aed51387d257044290d1dd85e8cc9a9d40ffad,unchanged by this PR. Both arms are on
f84eb66, the current upstream head; thebefore arm carries the measurement-only shim the docs section above records and the
after arm ran the survey unmodified. Both arms ran against the extension rebuilt
from that source.
All 467 overloads present in both arms, no verdict differing -- and the two runs
agree below the verdict too: every route carries the same status on every profile,
so all 3269 cells agree, and all 467 per-route running totals print identically,
[107/467] clamp_.Tensor strict=52 basic_only=11 failed=6 untested=38and[169/467] flip strict=99 basic_only=16 failed=10 untested=44included. That isalso what makes the run comparable despite only one arm carrying the shim.
At case level this pair moved nothing: both arms' census reads
PASS 1881, INVALID_CASE 1086, ERROR 151, WRONG 132, CRASH 14, TIMEOUT 5. The pairs recordedfor the two previous bases each moved exactly one cell, always a
2d-f32profile ofone of the two
index_copyoverloads, inside that overload's unchangedFAILEDverdict, and in opposite directions:
index_copy_PASS->WRONGon thepre-rebase base,
index_copyWRONG->PASSon892432b. Across all six runs thecensus takes exactly two values,
PASS 1881 / WRONG 132andPASS 1882 / WRONG 131, withINVALID_CASE 1086,ERROR 151,CRASH 14andTIMEOUT 5identical inevery one, so the only quantity that moves is which of those two profiles sits in
PASS. Six isolated re-runs of each overload on the post-fix tree with nothing elsechanged give
index_copyPASSfour times andWRONGtwice andindex_copy_WRONGfive times andPASSonce, withmax_diffbetween 1.9 and 4.6 whenever thecomparison does fail, so the case is unstable run to run rather than fixed or broken
by the route:
index_copy's synthesized index argument israndint(0, 2, ...), soduplicate indices make the comparison order-dependent. That is a harness artifact,
not a route or kernel effect.
docs/reference/operator-support.mdgains the full section and Update History rowwith the same figures.
Fixes #264
🤖 Generated with Claude Code