Skip to content

fix(comm): run collectives on their operand's device - #3

Closed
lvyufeng wants to merge 20 commits into
mainfrom
fix-comm-device-index
Closed

fix(comm): run collectives on their operand's device#3
lvyufeng wants to merge 20 commits into
mainfrom
fix-comm-device-index

Conversation

@lvyufeng

@lvyufeng lvyufeng commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the device-index bug that causes collectives to enqueue device-N buffers onto a device-0 stream, resulting in VMFault on FlagCX/DCU and silent wrong results on other backends.

Root Cause

csrc/runtime/accelerator/cuda/device.cc maintained a thread_local int gCurrentDevice shadow variable that GetDevice returned instead of asking the driver. Since flagos aliases the same physical GPUs as torch.cuda, and cudaSetDevice's current device is already thread-local, this shadow copy could only diverge:

  • torch.cuda.set_device(1) moves the driver's index without going through our SetDevice, leaving the shadow at 0
  • Any DeviceGuard then reads the stale 0 as "previous" and restores the process to a device that was never current
  • Observed symptom: allocating a flagos:1 tensor resets torch.cuda.current_device() back to 0

ProcessGroupFlagOS delegates to an inner backend (FlagCX or RCCL) that resolves its device from whatever is current, so rank 1's device-1 collective gets enqueued on the device-0 stream FlagCX cached. On DCU this triggers VMFault; on other hardware it silently computes wrong results or hangs.

Changes

  1. C++ fix (device.cc): Delete the shadow variable; make GetDevice call cudaGetDevice directly
  2. Dedup (process_group.py): Remove duplicate _allgather_base and _reduce_scatter_base definitions that were silently shadowing each other
  3. Regression test: tests/manual/test_comm_device_index.py deliberately calls only torch.cuda.set_device(rank) to catch this class of bug

Verification

Impact

This also fixes the device-index hazard that flagos-ai#54 addressed on the compute side (CallPythonOp_*). Both were symptoms of the same C++ accounting bug.

lvyufeng and others added 20 commits July 29, 2026 16:12
…flagos-ai#34)

* fix(dist): add _allgather_base/_reduce_scatter_base to ProcessGroupFlagOS

The functional all_gather_into_tensor / reduce_scatter_tensor APIs dispatch
to group._allgather_base / group._reduce_scatter_base (single flat tensor in
and out), which ProcessGroupFlagOS did not override -> 'No backend type
associated with device type flagos'. Wrap both, converting the flagos tensors
to the inner backend's device view like the other collectives.

Add tests/manual/metax/test_flagos_dist_live_metax.py: the MetaX counterpart
of the nvidia live test. Live-verified on 2xMetaX C550 via the NCCL(mccl)
fallback path (GEMS_VENDOR=metax, flagcx absent): all_reduce / broadcast /
all_gather / reduce_scatter_tensor and DDP grad sync ALL PASS.

* fix(metax): route torch.cuda.set_device to maca runtime for FlagCX

Under metax boxing, stock torch.cuda.set_device only bumps torch's own
device counter (torch._C._cuda_setDevice on the CPU wheel) and never
reaches maca's mcSetDevice, so torch.cuda.set_device(rank) leaves the
maca runtime on device 0 while torch.cuda.current_device() reports rank.

ProcessGroupNCCL(mccl) hides this by wrapping every op in a
CUDAGuard(tensor.device()); FlagCX does not -- it creates its collective
stream (mcStreamCreateWithFlags) on the *current* maca device before
binding the tensor's device, so on rank!=0 the stream lands on device 0
while the communicator/tensor are on device r, giving
'CUDA error: invalid resource handle'.

Patch torch.cuda.set_device to also move the maca runtime (via
_flagos.set_device) and point current_device at the maca-backed reader,
so the FlagCX metax path binds a consistent device. Verified 2xC550:
all_reduce/broadcast/all_gather/reduce_scatter + DDP grad sync ALL PASS
through the FlagCX (flagcx=yes) path.

* fix(metax): drop unused ProcessGroupFlagOS import in dist live test

The _describe_inner_backend helper imported ProcessGroupFlagOS but never
used it, tripping ruff F401 in CI. Remove the import.

* style(metax): ruff format dist live test print lines
* ci: use the PyTorch CUDA runner set

* test: expect CUDA fallback for recursive mul

---------

Co-authored-by: 马函廷 <mahanting@mahantingdeMacBook-Air.local>
…outing (flagos-ai#36)

Two independent fixes. The first is NOT DCU-specific -- reviewers on
CUDA/MetaX/Ascend should look at it, since it affects every backend that
runs the FlagGems Python path on more than one device.

1. FlagGems factory ops ignored their `device` argument (general
   multi-device bug)

CallPythonOp_Factory / CallPythonOp_LikeFactory injected a hardcoded
`device=flagos:0` kwarg, dropping the `device` the generated kernels
already had in scope. Two consequences:

  - torch.ones(4, device="flagos:1").device reported flagos:0.
  - the output was allocated on device 0 while gems' Triton kernel
    launched on the *current* device -- a cross-device write, which
    faults the GPU rather than raising. On DCU this surfaced as
    "Invalid address access, Error code: 3" / KERNEL VMFault, and it
    made every rank>0 worker crash under FlagGems since DDP workers
    build their tensors via factories.

Both callers now take an optional at::Device and resolve the index once:
the argument's index when it carries one, else c10::flagos::CurrentDevice(),
matching aten factory semantics (an index-less device means "current" --
the same rule csrc/aten/empty.cc applies via c10::device_or_default). The
Python call runs under a c10::DeviceGuard on the resolved device so gems'
internal torch.empty and the Triton launch agree. For *_like, an absent
device falls back to the source tensor's device ("same as self") before
the current-device default.

codegen_ops.py forwards the aten `device` arg at both emit sites; the 18
regenerated call sites (13 Factory + 5 LikeFactory) are the only change
to flaggems_python_kernels.cc. TensorToPython's CPU-scalar hop moves to
the current device too, so a scalar operand on device N no longer drags
the computation back to device 0.

2. comm layer did not know GEMS_VENDOR=hygon

_VENDOR_PROFILES had no hygon row, so DCU tripped the unknown-vendor
warning and fell back to guessing nvidia. Worse, with GEMS_VENDOR unset
_patch_flaggems_codegen_config() reached the ascend fallback on DCU
(is_nvidia_cuda_available() is False -- DTK has no libcuda.so, only
libgalaxyhip), selecting the HCCL profile and failing outright with
"no suitable inner backend for GEMS_VENDOR='ascend'".

Adds the hygon row (CUDA-ABI: zero-copy view + NCCL, which on DTK is
RCCL) and a DCU branch that sets GEMS_VENDOR=hygon, keyed on the existing
_build_accelerator() helper and placed before both the generic-NVIDIA
branch and the ascend fallback. setdefault, so an explicit GEMS_VENDOR
still wins. The transport itself needed no work.

Verified on Hygon DCU (DTK 26.04-rc4, 8x BW), GEMS_VENDOR left unset:

  - ones/zeros/randn/empty/rand/full/arange/eye/linspace and the *_like
    variants all land on flagos:1 with correct values; dispatch log
    confirms `ones -> flagos_python`, i.e. the gems path.
  - tests/manual/test_flagos_dist_live.py with FlagGems ON: all five
    collectives + DDP fwd/bwd pass at --world-size 2 and 8 (previously
    VMFaulted).
  - 88 passed across tests/unit + test_allocator + test_factory_ops +
    the new test_factory_device_index, with FlagGems both on and off.
  - full per-file ops sweep: 37/39 clean with FlagGems on, 38/39 with it
    off (test_full_cuda_coverage 46/46, test_ops 58 passed).

Pre-existing upstream failures, unchanged by this work and reproducible
on unmodified main: test_mul_dispatch::test_dispatch_log_flaggems_runtime
and the orphan/count checks in test_flaggems_conf_consistency (flagos-ai#28 routed
mul.Tensor to cuda but left its kFlagOsPython kernel registered).
Event: flagos.Event was a host-timestamp stand-in whose wait() was a
no-op, so cross-stream ordering silently raced. Wrap torch.cuda.Event
(a real maca event under boxing) instead. Boxing patches
torch.cuda.current_stream into a non-Stream shim (triton launch only),
which breaks Event.record()/stream() -- so resolve the real current
stream straight from torch._C._cuda_getCurrentStream and reimplement the
stream() context manager via _cuda_setStream, bypassing the shim.

pin_memory: _to_copy hard-rejected pin_memory=True. The host allocator
(cudaMallocHost), isPinnedPtr and _pin_memory dispatcher already work, so
allow pin_memory=True for a CPU destination (pin the result via
at::_pin_memory) and keep rejecting it for device destinations.

Verified on 2xC550 (tests/manual/metax/test_event_pin_metax.py, 11/11).
CI runs `lint` first and every other job declares `needs: lint`, so a
formatting slip skips the build and integration jobs entirely -- a red lint
tells you nothing about whether the change actually works. Cheap to avoid by
running the gate locally first.

Records the contract from .github/workflows/lint.yml: ruff==0.15.12 (pinned,
since formatting rules shift between releases), `ruff check .` plus
`ruff format --check .`. Also notes two things that cost time in practice:
`ruff format --check` reports only a count in the CI summary so you need
`--diff` locally to see what it wants, and on vendor boxes where the torch
install is in the system interpreter (DCU/DTK, where bare `python` is conda
with no torch) the interpreter has to be spelled out or ruff lands in the
wrong environment.

Lives in .claude/skills/ rather than CLAUDE.md because CLAUDE.md is
gitignored here -- a skill is the mechanism that actually travels with the
repo to other machines.
…njection (flagos-ai#39)

Unifies the native-CUDA RNG half with the FlagGems RNG half so
torch.manual_seed makes ALL flagos RNG reproducible. Prior work unified only
the FlagGems Python hot path via torch.cuda.default_generators; native ops
routed `= cuda` (normal_/bernoulli/randint/random_/log_normal_/randperm...)
still boxed to CUDA and drew from ATen's getDefaultCUDAGenerator(), which is
unreachable from torch.manual_seed on the CPU-torch + external libtorch_cuda.so
wheel (torch._C has no _cuda_manualSeed binding). Result was two disjoint RNG
worlds.

Approach -- C++ generator injection:

- GetFlagosDefaultCudaGenerator(int64_t idx): new helper in python_op_caller.cc.
  Under the GIL, fetches torch.cuda.default_generators[idx] -- the same
  per-device torch.Generator(device="cuda") the vendor compat shim installs and
  FlagGems reads -- and returns it as at::Generator (process-lifetime cached by
  index).
- Codegen (scripts/codegen_ops.py): for every native kernel whose schema carries
  Generator?, emit `if (!generator.has_value()) generator =
  GetFlagosDefaultCudaGenerator(<dev>)` before the at::<op>() call. Covers 80
  kernels across functional/inplace/out/tuple/factory templates. No-op when the
  caller passes a generator (backward compatible).
- Generator-less factory overloads (randint/randint.low/randperm):
  torch.randint(...) / torch.randperm(...) dispatch to overloads whose schema
  has no Generator? arg, so the rule above did not reach them. These bases each
  expose a sibling overload with Generator? right after the size arg (per
  ATen/ops/{randint,randperm,rand,randn}.h), so inject the shared generator
  there. randperm falls out for free (its randomness is an internal
  torch.randint).
- CMakeLists: build python_op_caller.cc whenever CUDA_KERNEL=ON (cuda_kernels.cc
  now calls the helper regardless of the FlagGems flags).

No routing/config changes -- native RNG stays `= cuda`; the unification is
internal to those kernels.

Verified on A100 (FLAGOS_USE_FLAGGEMS=1): rand/randn/rand_like/uniform_/
exponential_/multinomial (FlagGems) AND normal_/bernoulli/random_/log_normal_/
cauchy_/geometric_ (native) AND the former gaps randint/randint.low/randperm are
all reproducible and seed-sensitive under torch.manual_seed. Distributions
correct; explicit generator= still honored. No regression: rng_dispatch 10
passed, flaggems_python 37 passed, native 335 passed/58 skipped/3 xpassed
(pre-existing conv1d segfault excluded).

Includes design + implementation-plan docs under docs/superpowers/.
…agos-ai#40)

* feat: add Moore Threads MUSA backend (musart runtime + mudnn kernels)

Adds ACCELERATOR=musa, taking the native operator route like Ascend and GCU:
generated kernels over mudnn, the vendor's torch-independent kernel library.
There is no libcudart shim on this platform, so the runtime layer is a native
port onto the musa* API and no CUDA boxing kernels are built.

mudnn (not at::musa::*) is the integration point on purpose. torch_musa also
exposes a flat at::musa::* API, which is cheaper to wrap but lives in
libmusa_python.so -- that links against torch and embeds its C++ object layout,
pinning the plugin to one exact torch build. sizeof(c10::MessageLogger) changed
408 -> 400 between 2.9.1 and 2.10, which corrupts the vendor binary's stack
(tensor.sum() segfaults inside empty_musa). libmudnn.so has no torch symbols, so
nothing here embeds torch's object layout, and the build needs only the MUSA
toolkit -- no torch_musa package, no extracted vendor .so.

Coverage is 64 generated ops (scripts/codegen_mudnn.py, category-driven like
codegen_gcu.py) plus 2 hand-written convolution kernels; everything else
reaches the cpu_fallback. Convolution is hand-written because ATen's default
for convolution_overrideable raises rather than being boxable to CPU.

Kernel design follows what mudnn was measured to do, not what the headers
suggest:

- mudnn Tensors honour strides including 0-strides, so broadcasting is expand()
  alone and strided inputs are read in place -- no .contiguous() as GCU needs.
  MudnnCopy uses this for _copy_from/contiguous via Unary IDENTITY/CAST.
- int64 works across Unary/Binary/Reduce/MatMul, so there is no int64 fallback
  branch, but bool is rejected for arithmetic modes and needs its own predicate.
- TF32 defaults on in mudnn while torch defaults matmul TF32 off; the handle now
  follows at::globalContext().allowTF32CuBLAS(), without which a 64x64 float mm
  drifts ~2e-2 from CPU.
- mudnn's Convolution is 2D-only, so conv1d runs as 2D with a unit H dim and
  conv3d falls back. The forward algorithm is probed and cached per shape, since
  GetRecommendForwardAlgorithm can name one the Run then rejects.
- Reduce raises SIGFPE -- uncatchable, not a status -- on a multi-dim reduce of
  a fully 0-strided input, which conv bias gradients hit via ones.expand(); such
  inputs are materialized first.

Verified on 8x MTT S5000 against the supported target, torch 2.10.0+cpu: clean
build and 478 passed / 209 skipped / 3 xpassed, no failures (the 9 errors are
tests/integration/test_qwen3_* failing setup because `transformers` is not
installed on this box). MLP and conv training loops match CPU to 1.2e-07. The
same tree was also run clean against 2.9.1, but 2.10.0+cpu is the version that
is supported and tested.

Also moves the PrivateUse1-already-claimed check ahead of the _C import: when
torch_musa happens to be installed, its autograd fallback registration turns a
late check into an uncatchable std::terminate instead of an actionable error.

* fix(allocator): zero-byte allocations must report the current device

CachingDeviceAllocator::allocate hardcoded index 0 for nbytes == 0, so
torch.empty(0, device="flagos:1") came back as flagos:0. That leaks into the
composite factories: arange and eye are implemented as `at::empty({0}, options)`
followed by an `_out` variant, so both dropped their device index. linspace
allocates `steps` elements up front, which is why it was unaffected and the bug
went unnoticed.

Use the current device instead -- empty_memory_format has already installed a
DeviceGuard for the requested device, so it is the right index. A zero-byte
request should not require a live runtime, so a failed index query degrades to 0
rather than raising, and the TORCH_CHECK stays on the path that actually
allocates.

Fixes test_factory_device_index.py::test_{arange,eye}_honors_device_index.
Not backend-specific: the passthrough DeviceAllocator already did this
correctly, so only the caching path was wrong.
…ily) (flagos-ai#43)

Grows GCU coverage from 67 to 101 topsaten kernels, all via
scripts/codegen_gcu.py. Ops without a working vendor kernel stay unregistered
on PrivateUse1 and keep reaching cpu_fallback, so nothing regresses.

Tier-1 compute (16 ops, 14 new categories): amax/amin, tril/triu, flip, clamp,
addmm + addmm.out, cat, zeros_like/ones_like, native_layer_norm,
_softmax_backward_data, silu_backward, mse_loss/mse_loss_backward.

foreach (18 ops, 11 new categories): _foreach_add_/sub_/mul_/div_ across the
Scalar, List, Tensor and ScalarList overloads, _foreach_neg_, _foreach_sqrt_,
_foreach_sqrt, _foreach_lerp_.Scalar, and _foreach_addcmul_/addcdiv_ in both
Scalar and ScalarList forms.

The foreach set was chosen by tracing a real workload rather than by guessing:
a TorchDispatchMode over two AdamW steps on a 2-block transformer showed the
foreach ops were the only remaining *compute* on the fallback path, with every
optimizer step copying all parameters to the host and back. That trace now
reports 19 falling-back ops instead of 27, and the remainder are view/metadata
ops plus three broken vendor kernels.

Two hardware findings shaped the implementation, both probed before any codegen:

  - topsaten's foreach ops accept an output list that aliases an input, so the
    in-place _foreach_*_ kernels update parameters directly with no temporary.
    They also broadcast a 1-element rhs across the list, which is what
    _foreach_add_.Tensor needs (aten requires that operand 0-dim; the wrapper
    presents rank-0 as shape {1}).
  - topsatenNativeLayerNormBackward rejects every output_mask ("LNB Output mask
    is not supported now!") yet still returns TOPSATEN_STATUS_SUCCESS while
    writing nothing, so a caller would see uninitialized gradients rather than
    an error. It is deliberately absent from OPS, with a comment; the forward is
    correct and is registered.

A CPU fallback must never call back into the same aten op on a flagos tensor --
it re-enters the kernel and recurses until the stack overflows. zeros_like hit
this and core-dumped; the foreach fallbacks loop the equivalent per-tensor
method (t.add_, t.addcmul_) instead of at::_foreach_*. Also fixes ones_like
under memory_format=Preserve, which autograd passes for every seed gradient and
which at::empty rejects outright -- that broke .backward() entirely.

New in topsaten_common.h: TopsatenTensorList (owns one wrapper per list
element so every sizes/strides array outlives the call), IsForeachEligible
(uniform device, contiguous, zero offset, non-empty, representable dtype), and
DtypeLowest/DtypeHighest for filling in an absent clamp bound.

Also adds the missing "gcu" row to the test suite's _PLATFORM_SKIP_MARKERS.
Since flagos-ai#40 the ops conftest resolves the platform from lib/flagos_platform,
which reads "gcu" here -- a key absent from the table, so GCU got an empty skip
list and ran every other backend's routing tests. Those were passing only
because the ops they exercise had no GCU kernel and raised the expected
"backend not registered"; adding cat/ones_like/silu_backward made three of them
fail for the right reason. GCU skips the same markers MUSA does (plus musa),
for the same reason: the tops stack ships no CUDA runtime, so no boxing kernels
are compiled and FlagGems is not built.

Verified on an S60: every kernel compared against CPU including
non-contiguous (transposed) inputs, broadcast, keepdim, negative dims,
fp16/bf16, and int64 fallback; in-place foreach ops additionally checked for
mutating the original tensors rather than a temporary. An 8-step AdamW run
matches the CPU reference to 1.2e-07 with monotonic loss. flagos:1 and flagos:3
agree with CPU. Against a pristine build of the parent commit this branch adds
no test failures and removes 95: 6 failed / 387 passed / 294 skipped, versus
101 failed / 526 passed / 60 skipped. The 6 remaining are the pre-existing
conv1d cases needing convolution_overrideable; the 9 errors are the absent
transformers package.
…er CI (flagos-ai#49)

The unified-RNG contract -- one `torch.manual_seed` reaching every RNG op on the
flagos device -- had no CI protection at all, and two codegen templates still
leaked.

Injection gaps. Generator-less RNG overloads carry no `Generator?` in their
schema, so `_generator_inject_line` never fires and they fall back to ATen's own
default CUDA generator, unreachable from `torch.manual_seed` on a CPU-torch
wheel. `torch.randint_like(...)` and every RNG out-variant dispatch to exactly
such overloads. The fix has to be made once per template because the ATen
insertion point differs each time: `*_like` takes the generator before `dtype`
(it carries `self`, so it lands on gen_functional_pure rather than gen_factory),
while out-variants take it before the first of names/memory_format/out. That
second one covered 11 kernels -- rand/randn `.out` and `.names_out`,
rand_like/randn_like `.out`, randint `.out`/`.low_out`, randint_like
`.out`/`.low_dtype_out`, randperm `.out`. The three base sets are now
module-level constants, one per template, so all three positions read together.

`_is_in_bad_fork`. `torch.random._seed_custom_device` needs both it and
`manual_seed_all` to seed a custom device; only the latter existed, so every
`torch.manual_seed()` warned "Set seed for `flagos` device does not take effect"
-- false, since seeding really goes through the patched
`torch.cuda.manual_seed_all`. Adding it silences the warning and makes
`flagos.initial_seed()` track the global seed.

Tests. The old file was marked `flaggems_python` with no `main_ops`, while CI
selects only `-m main_ops` and `-m "flaggems and main_ops"` -- so it never ran.
It also skipped itself entirely without FLAGOS_USE_FLAGGEMS, on the premise that
the native path could not be reproducible; C++ generator injection made that
premise obsolete. Rewritten to run under both configs (the native-path injection
is only exercised by the vendor run) and to cover 39 ops grouped by the mechanism
each stresses, plus the seed plumbing itself, multi-device generators, and
distribution correctness. CI now collects 105 RNG tests in the vendor job and 2
in the FlagGems job, up from 0; no workflow change was needed, the markers are
the wiring.

Two gaps injection cannot reach are recorded as non-strict xfails rather than
left implicit: `native_dropout` has no `Generator?` anywhere in its ATen schema,
so the vendor path has no argument to inject into; and FlagGems `multinomial`
launches its Triton kernel against the wrong device for any index != 0, a
device-context bug on that path rather than an RNG one.

Verified on 8xA100 under both configs: 102 passed/2 skipped/1 xfailed (vendor)
and 104 passed/1 xfailed (FlagGems). Regression clean -- main_ops 115 passed,
flaggems main_ops 14 passed, flaggems_python 27 passed, native 445 passed/3
xpassed (pre-existing conv1d segfault excluded). Both ruff gates pass.
…ified broken) (flagos-ai#48)

Re-ran codegen discovery against flag_gems 5.4.0dev and executed every newly
routed op against a CPU reference on Hygon DCU. discover_flaggems_ops() gates
only on static arity/type inspection of the gems function, so it cannot tell
whether the call actually works: of 98 new candidate routes, 26 (27%) fail at
runtime. All 26 pass on the cuda route and fail as flagos_python, so each is a
genuine route regression rather than pre-existing breakage.

Net +72 routes: 318->390 (nvidia), 316->388 (dcu), 305->377 (metax).

New flaggems_runtime_broken set, grouped by cause:
  - gems declares `out` as a required keyword-only arg (14) -- the
    out_variant caller passes only non-out args and copy_'s the result, so
    these need a kwarg-out category in the C++ caller to be recovered.
  - gems asserts device.type == "cuda" (8) -- a PrivateUse1 tensor trips it.
  - DTK triton (hcu) cannot compile the kernel (2): gcd, gcd.out.
  - wrong numerics on the gems path (2): leaky_relu_,
    adaptive_max_pool3d_backward.

Kernels stay generated (417) and reachable via FLAGOS_OP_<op>=flagos_python;
only the default route goes to cuda, same treatment as
flaggems_recursive_fallback.

Two codegen fixes found along the way:

  - gen_inplace() did not box optional<Tensor> args, so the guard saw a mix of
    boxed and unboxed tensors. cuda_kernels.cc had a hand-applied fix for
    ClampInplaceTensor that any regeneration silently reverted; the generator
    now emits it. This is the only cuda_kernels.cc change.
  - conf generation stripped the Apache header off the three checked-in confs
    on every run; it is now emitted.

torch_fl/codegen_skip_ops.txt gains the 64 vendor-only native_fuse_* ops the
DTK wheel's bundled torchgen yaml declares, which would otherwise put
DTK-only includes into the shared cuda_kernels.cc. Codegen output is now
platform-neutral: FLAGOS_CODEGEN_ALL=1 produces no diff on either wheel.

test_flaggems_conf_consistency: generalize the AST reader to both forced-cuda
sets, and anchor _wrapper_to_dispatcher's regex to `Wrapper` -- the old
pattern matched the file's own header comment ("m.impl() lines.") and ate the
first real wrapper, which surfaced now that _adaptive_avg_pool2d is routed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…flagos-ai#52)

The three `default_generators` shims (metax compat, cuda compat, and the
flagos module) stand in for a value upstream types as a *tuple*, so callers
are entitled to iterate it, slice it or wrap it in `list()`. All three were
list-like proxies defining only `__getitem__`/`__len__`, and `__getitem__`
was unbounded.

With no `__iter__`, Python falls back to the legacy iteration protocol:
call `__getitem__(0, 1, 2, ...)` until IndexError. An unbounded
`__getitem__` never raises, so `for g in torch.cuda.default_generators`
became an infinite loop that allocated a fresh CUDA generator on every
step. It presented as a hang rather than an error, which is why nothing
surfaced it -- found while walking the generator list on an 8-card C550,
where the process sat at 200% CPU indefinitely. The flagos variant did not
hang but was equally wrong: out-of-range indices surface as a RuntimeError
from C++, which aborts iteration instead of ending it.

All three now define `__iter__` and bounds-check `__getitem__`, raising
IndexError past the device count and wrapping negative indices the way the
tuple they replace does. Slices are supported for the same reason.

Regression test is parametrized over both the `torch.cuda` and `flagos`
shims and asserts all three properties; verified to hang on the unfixed
tree and pass on the fixed one.

Verified on 8xC550 (metax boxing): RNG suite 104 passed/2 skipped/1 xfailed
(vendor) and 106 passed/1 xfailed (FlagGems). Wider probe of 91 RNG ops
covering every overload family, dtype and nn.init entry point: 89/91 clean
on the vendor path (the 2 are the known native_dropout schema gap) and
91/91 under FlagGems. Distribution moments checked against theory on all
8 cards. Both ruff gates pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lagos-ai#47)

The skill covered lint but said nothing about the base the lint runs on, and
lint-before-rebase is wasted work: a rebase can reintroduce lint errors or pull
in regenerated files. Reorder into an explicit 4-step sequence with rebase
first, and document the three ways the rebase actually goes wrong here:

  - A stale local flagos/main ref. This repo moves fast (ten commits landed in
    two days recently) and the network to github is slow enough that fetches
    get skipped, so the cached ref silently drifts. Check the ref's *date*,
    not just its sha.
  - A commit that already landed upstream. Squash-merges change the sha, so git
    cannot dedupe it and the rebase applies the same change twice -- the whole
    PR comes back conflicted. Drop the local commit; upstream's copy wins.
  - Generated-file conflicts. Take upstream's side, port only the generator
    change, re-run codegen, and verify a second run leaves no diff, rather than
    hand-merging conflict markers in csrc/aten/generated or the backend confs.

Also warn that `git checkout <commit> -- <file>` on a hand-written file
discards upstream's edits to it silently; `git apply --3way` surfaces the
conflict instead. Checklist gains the rebase, commit-range and codegen
idempotency steps.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lagos-ai#42)

* ci: add configured MetaX build and integration workflows

* ci: fix MetaX workflow input expressions

* ci: persist isolated MetaX Python path

* ci: preserve platform environment in integration tests

* ci: isolate CUDA wheel from vendor torch

* ci: install CUDA integration test dependencies

* ci: tolerate CUDA images without standalone libc10_cuda

* ci: pin CUDA CI image digest

* ci: build MetaX integration wheel locally

* ci: diagnose CUDA library layout

* ci: fix native library staging

* ci: load isolated native runtimes

* ci: build CUDA integration wheel locally

* ci: stage CUDA runtime dependencies

* ci: diagnose MetaX runtime symbols

* ci: retain MetaX CUDA runtime dependency

* fix: provide MetaX generator ABI shim

* fix: use vendor CUDA generator for MetaX boxing

* ci: capture first MetaX operator failure

* fix(rng): close the last two generator-injection gaps and put RNG under CI (flagos-ai#49)

The unified-RNG contract -- one `torch.manual_seed` reaching every RNG op on the
flagos device -- had no CI protection at all, and two codegen templates still
leaked.

Injection gaps. Generator-less RNG overloads carry no `Generator?` in their
schema, so `_generator_inject_line` never fires and they fall back to ATen's own
default CUDA generator, unreachable from `torch.manual_seed` on a CPU-torch
wheel. `torch.randint_like(...)` and every RNG out-variant dispatch to exactly
such overloads. The fix has to be made once per template because the ATen
insertion point differs each time: `*_like` takes the generator before `dtype`
(it carries `self`, so it lands on gen_functional_pure rather than gen_factory),
while out-variants take it before the first of names/memory_format/out. That
second one covered 11 kernels -- rand/randn `.out` and `.names_out`,
rand_like/randn_like `.out`, randint `.out`/`.low_out`, randint_like
`.out`/`.low_dtype_out`, randperm `.out`. The three base sets are now
module-level constants, one per template, so all three positions read together.

`_is_in_bad_fork`. `torch.random._seed_custom_device` needs both it and
`manual_seed_all` to seed a custom device; only the latter existed, so every
`torch.manual_seed()` warned "Set seed for `flagos` device does not take effect"
-- false, since seeding really goes through the patched
`torch.cuda.manual_seed_all`. Adding it silences the warning and makes
`flagos.initial_seed()` track the global seed.

Tests. The old file was marked `flaggems_python` with no `main_ops`, while CI
selects only `-m main_ops` and `-m "flaggems and main_ops"` -- so it never ran.
It also skipped itself entirely without FLAGOS_USE_FLAGGEMS, on the premise that
the native path could not be reproducible; C++ generator injection made that
premise obsolete. Rewritten to run under both configs (the native-path injection
is only exercised by the vendor run) and to cover 39 ops grouped by the mechanism
each stresses, plus the seed plumbing itself, multi-device generators, and
distribution correctness. CI now collects 105 RNG tests in the vendor job and 2
in the FlagGems job, up from 0; no workflow change was needed, the markers are
the wiring.

Two gaps injection cannot reach are recorded as non-strict xfails rather than
left implicit: `native_dropout` has no `Generator?` anywhere in its ATen schema,
so the vendor path has no argument to inject into; and FlagGems `multinomial`
launches its Triton kernel against the wrong device for any index != 0, a
device-context bug on that path rather than an RNG one.

Verified on 8xA100 under both configs: 102 passed/2 skipped/1 xfailed (vendor)
and 104 passed/1 xfailed (FlagGems). Regression clean -- main_ops 115 passed,
flaggems main_ops 14 passed, flaggems_python 27 passed, native 445 passed/3
xpassed (pre-existing conv1d segfault excluded). Both ruff gates pass.

* fix(rng): make default_generators iterable instead of looping forever (flagos-ai#52)

The three `default_generators` shims (metax compat, cuda compat, and the
flagos module) stand in for a value upstream types as a *tuple*, so callers
are entitled to iterate it, slice it or wrap it in `list()`. All three were
list-like proxies defining only `__getitem__`/`__len__`, and `__getitem__`
was unbounded.

With no `__iter__`, Python falls back to the legacy iteration protocol:
call `__getitem__(0, 1, 2, ...)` until IndexError. An unbounded
`__getitem__` never raises, so `for g in torch.cuda.default_generators`
became an infinite loop that allocated a fresh CUDA generator on every
step. It presented as a hang rather than an error, which is why nothing
surfaced it -- found while walking the generator list on an 8-card C550,
where the process sat at 200% CPU indefinitely. The flagos variant did not
hang but was equally wrong: out-of-range indices surface as a RuntimeError
from C++, which aborts iteration instead of ending it.

All three now define `__iter__` and bounds-check `__getitem__`, raising
IndexError past the device count and wrapping negative indices the way the
tuple they replace does. Slices are supported for the same reason.

Regression test is parametrized over both the `torch.cuda` and `flagos`
shims and asserts all three properties; verified to hang on the unfixed
tree and pass on the fixed one.

Verified on 8xC550 (metax boxing): RNG suite 104 passed/2 skipped/1 xfailed
(vendor) and 106 passed/1 xfailed (FlagGems). Wider probe of 91 RNG ops
covering every overload family, dtype and nn.init entry point: 89/91 clean
on the vendor path (the 2 are the known native_dropout schema gap) and
91/91 under FlagGems. Distribution moments checked against theory on all
8 cards. Both ruff gates pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: nate.river <lvyufeng@cqu.edu.cn>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lagos-ai#41)

Wires torch.compile into the flagos (PrivateUse1) backend by registering
flagos with TorchInductor as a real GPU device, so the traced graph is
handed to compile_fx unchanged and inductor emits Triton kernels that
operate on flagos tensors directly.

The earlier approach rewrote the graph and its example inputs to cuda
before compiling. That is not just a copy per call: at::getAccelerator()
is PrivateUse1/flagos in this build, and torch::autograd::Node::stream()
only yields a stream when a node's input device type equals the
accelerator. A cuda-rewritten graph therefore produces stream-less
autograd nodes, and AOT autograd's backward trace inside compile_fx trips
opt_ready_stream && opt_parent_stream (engine.cpp:1085) -- the cause of
8 of the 11 test failures. Verified by differential test: eager backward
on plain cuda fails identically with torch.compile never involved.

Registration surface (device_interface.py, inductor_codegen.py):

* GPU_TYPES gains "flagos" in place -- is_gpu() is a membership test on
  that list object, and without it inductor takes the C++/CPU codegen
  path and never emits Triton. get_gpu_type()'s functools cache is primed
  while the list is narrowed, since it asserts at most one GPU type is
  available and the torch.cuda shim reports available too.
* DeviceInterface subclass: device state from torch.flagos, hardware
  properties from torch.cuda (same physical GPU, same allocator).
* DeviceProperties.create reports flagos as cuda at the Triton boundary.
  Triton's NVIDIA backend hard-checks target.backend == "cuda", so a
  literal "flagos" finds 0 compatible backends. Inductor already does
  this rewrite in the opposite direction for ROCm (hints.py:149).
* Device op overrides + scheduling/wrapper codegen: the stock CUDA/Triton
  pipeline under the "flagos" key, also published on torch.flagos for
  inductor's official PrivateUse1 hook.

Two generated-kernel bugs that only surface under compilation:

* detach re-dispatched into itself. The kernel called at::detach(self),
  also registered on PrivateUse1, so it dispatched straight back. Eager
  hid the recursion because DeviceBoxingGuard rewrites self's device
  metadata first; under FakeTensor it cannot, since the Python dispatch
  key sits above the backend key. Dynamo traces every nn.Linear through
  detach, so this was a stack-overflow segfault at trace time. Now emits
  at::native::detach (NATIVE_DIRECT_VIEW_OPS).
* gen_inplace passed only plain at::Tensor args to DeviceBoxingGuard, so
  clamp_.Tensor handed unboxed flagos min/max to a CUDA self and crashed.
  Optionals are now materialized into holders, matching gen_functional_pure.

Both regressions are covered by tests that were confirmed to fail (segfault
at the exact asserting line) against a build with the fixes reverted.

CPU-torch wheel accommodations, since torch.cuda's Python layer was frozen
without CUDA: re-attach CudaInterface.get_raw_stream (binding exists, the
import-time _is_compiled() probe left it None), route torch.cuda.memory_*
to the flagos allocator that backs the same pool, hand out flagos
Event/Stream in place of the dummy base classes, force triton.cudagraphs
off (torch.cuda.CUDAGraph raises on construction) and use_static_cuda_launcher
off (not built).

flagos_compile_backend now accepts the mode/options/dynamic kwargs dynamo
forwards to named backends and expands them into compile_fx config_patches,
rather than mutating inductor's global config.

Tests: test_compile.py 12 passed / 1 skipped (was 2 passed / 8 failed);
test_clamp_dispatch.py 15 passed; ops dispatch sweep 358 passed;
test_ops.py 58 passed; allocator/factory/fallback/unit 73 passed.

Docs updated to drop the device-aliasing description and the unmeasured
performance-parity figures; benchmarking remains open work.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s-ai#54)

Two independent multi-device bugs, both reached through
csrc/aten/backends/flagos/python_op_caller.cc.

1. Missing DeviceGuard in the non-factory callers

   flagos-ai#36 gave the two factory callers a DeviceGuard. The other 16 never got
   one, so the same hazard remained reachable from the other side: gems
   allocates its intermediates with `device=input.device` but launches
   Triton on the *current* device. Call an op on a tensor living on
   device 1 while device 0 is current and the kernel reads and writes
   across devices.

   That does not raise -- it faults the GPU (segfault / KERNEL VMFault /
   "Invalid address access"), so it presents as a process kill with no
   Python traceback. Found via
   `torch.multinomial(torch.ones(10, device="flagos:1"), 5)` under
   FLAGOS_USE_FLAGGEMS=1, which dies while device 0 works and while the
   boxing path works on both.

   Every caller now takes a c10::OptionalDeviceGuard resolved from its
   first tensor operand that names a device (DeviceOfTensor /
   DeviceOfArgs / DeviceOfFirst). Non-tensor leading args are skipped, so
   `topk(values, k)` still resolves correctly. nullopt leaves the current
   device alone, so single-device callers are unaffected.

   This also fixes TensorToPython's CPU-scalar hop, which resolves to the
   current device: under the guard that is now the operand's device
   rather than whatever happened to be current at entry.

   Not DCU-specific -- any backend running the FlagGems Python path on
   more than one device hits it.

2. dcu linked with an undefined GetFlagosDefaultCudaGenerator

   Pre-existing, and the reason the fix above could not be built and
   tested on dcu at all. The flagos backend was excluded on
   `NOT FLAGGEMS_KERNEL AND NOT FLAGGEMS_PYTHON AND NOT CUDA_KERNEL`, but
   generated/cuda_kernels.cc is not governed by CUDA_KERNEL -- it lives
   under aten/generated/ and is dropped per-accelerator (gcu, musa)
   instead. Since the unified-RNG generator injection its native RNG
   kernels call GetFlagosDefaultCudaGenerator() unconditionally, which
   python_op_caller.cc defines.

   dcu is the one combination that referenced it without defining it:
   CUDA_KERNEL=OFF and both FLAGGEMS_* OFF, yet cuda_kernels.cc still
   compiled, because the DTK torch wheel is hipified and registers its
   HIP kernels under the CUDA dispatch key. musa sets the same three OFF
   but also drops cuda_kernels.cc, so it escaped. Result:

     ImportError: libtorch_fl.so: undefined symbol:
     _ZN2at6native6flagos29GetFlagosDefaultCudaGeneratorEl

   The exclusion now runs after the per-accelerator rules and tests the
   actual source list for cuda_kernels.cc rather than the CUDA_KERNEL
   option. Verified by configuring both ways: dcu goes BROKEN -> ok,
   tsingmicro stays ok, gcu/musa still exclude the file.

   flagos-ai#42's MetaX case is the same shape of exception, so it becomes a third
   named consumer of the rewritten condition rather than an append inside
   the removed block. (Its cuda_kernels.cc is in the source list anyway,
   so the check would already keep the file; the clause stays for intent.)
   flagos-ai#42's libtorch_cuda.so link block is untouched.

Testing (DCU, 8 devices)

  tests/integration/test_compute_device_index.py -- new, the non-factory
  companion to test_factory_device_index.py. Pins the current device to 0
  and drives one op per caller shape on flagos:1, asserting values and
  not just placement, since a cross-device read returns whatever the
  other device's memory holds. Confirmed to catch the bug: with the
  guard removed from CallPythonOp_Generic alone, the multinomial case
  dumps core.

  compute + factory + rng, FlagGems   140 passed, 1 xpassed
  compute + factory + rng, boxing     138 passed, 2 skipped, 1 xpassed
  ops sweep, FlagGems, per file       455 passed, 0 failures, 0 crashes
  conf consistency                    6 passed
  default dcu build (FLAGGEMS_PYTHON=OFF) builds and imports
  ruff 0.15.12 check + format         clean

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gos-ai#53)

* feat: adapt flagos CUDA backend to torch 2.11 via schema codegen

Port the codegen approach from 2.13 onto this branch: generate all 71 CUDA
boxing ops from native_functions.yaml via torchgen, replacing hand-written
per-op .cu/.cc + structured_ops.

- Bring over scripts/codegen_ops.py + generated/, device_boxing.h,
  dispatcher.h, refactored register.cc, external-libtorch scripts/docs, skill
- Delete 123 hand-written kernel files superseded by codegen
- Fix test_dispatch_log_bmm_out_flagos_default marker (cuda -> flaggems)

Generated products are byte-identical to 2.13 (same native_functions.yaml
schema for these 71 ops; ARRAYREF_OPS needs no change). codegen validated
on torch 2.11 torchgen. End-to-end CPU-only + external libtorch_cuda.so
verification deferred until download.pytorch.org (the +cpu wheel source)
is reachable again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: register full CUDA op set into flagos backend via codegen enumeration

Expand codegen from the hand-listed 71-op conf to the full CUDA dispatch
set with automatic filtering. backends_cuda.conf now routes 1824 ops to
the boxing CUDA kernel (zero hand-written kernels).

Enumeration predicate is a strict superset of the original 71-op conf
(0 missing): direct CUDA kernel union structured_delegate-with-CUDA-target
union composite_explicit_autograd. Ops the templates cannot express are
auto-skipped and fall back to cpu_fallback, so coverage only grows.

Key changes:
- codegen_ops.py: FLAGOS_CODEGEN_ALL full-enumeration mode; authoritative
  torchgen predicates (part_of_structured_group -> IListRef vs ArrayRef,
  use_const_ref_for_mutable_tensors -> mutable out param signature),
  root_name for ATen/ops headers, leading/trailing-underscore name
  disambiguation, compute-factory CUDA-device redirect (fixes randn 0-dim
  garbage), try/except auto-skip in all mode.
- codegen_skip_ops.txt: ~211 ops the templates cannot express
  (multi-out, exotic signatures, dunder shifts, const-ref out variants).
- test_full_cuda_coverage.py: 46 sampling tests across unary/binary/
  reduction/shape/factory/foreach with CPU cross-check + dispatch routing
  assertions + randn 0-dim regression guard.
- Fix silu_backward / nll_loss_backward tests to build inputs on CPU then
  move to device, instead of relying on cross-device same-seed randn
  (which no longer matches now that randn correctly uses the CUDA RNG).

Full op suite: 311 passed, 0 failed, 64 skipped, 3 xpassed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cuda): delegate flagos allocation to CUDACachingAllocator

The flagos (PrivateUse1) CachingDeviceAllocator maintained its own block
pool, so compute-factory ops (torch.randn/mm/etc.) — generated as CUDA
boxing kernels that allocate straight on the CUDA device — bypassed it
entirely. flagos memory_stats saw only `at::empty` allocations (~3% of the
real footprint) and empty_cache could not release the boxed-kernel memory.

Since flagos and CUDA share the same physical GPU memory (boxing only
relabels the device, no copy), route ALL flagos allocation through the same
c10::cuda::CUDACachingAllocator that boxed kernels already use. Now empty +
boxed outputs live in one pool: memory stats reflect the true footprint,
empty_cache works, and OOM-retry is unified.

- DeviceMemoryInterface: add opt-in caching-delegation API
  (provides_caching / caching_alloc / caching_free / caching_empty_cache /
  caching_record_stream / caching_get_stats / caching_reset_peak_stats),
  default off so Ascend and other backends keep the self-built block pool.
- CudaDeviceMemory: implement the API over CUDACachingAllocator
  (raw_alloc / raw_delete / emptyCache / getDeviceStats / resetPeakStats),
  with a lazy once_flag init(cudaGetDeviceCount()) — no Python lazy-init.
- CachingDeviceAllocator: allocate / empty_cache / record_stream / get_stats
  / reset_stats branch to the backend when provides_caching(); add
  delegated_deleter for the delegation path.
- Extract AllocatorStats into allocator_stats.h to break the include cycle
  between device_memory_interface.h and caching_device_allocator.h.
- CMake: define C10_CUDA_NO_CMAKE_CONFIGURE_FILE (non-Ascend) since the CPU
  torch wheel ships c10/cuda headers but not cuda_cmake_macros.h. Still links
  torch_cpu only; CUDA symbols resolve at runtime from the preloaded
  libtorch_cuda.so (CPU-torch + external-libtorch scheme unchanged).

Verified: torch.randn now tracked (allocated_bytes 0 -> 1048576),
empty_cache releases reserved (86MB -> 20MB). Regression green — ops 426
passed, allocator 11 passed (test_allocator.py reverted to torch.randn),
Qwen3 infer/train + fallback_trace 9 passed.

* feat(cuda): recover ~200 skipped ops via codegen template generalization

Generalize three codegen templates in scripts/codegen_ops.py so operators
that previously fell through to cpu_fallback are boxed directly:

- gen_out_variant now supports N mutable Tensor& outputs by calling
  at::<base>_outf(...) in faithful schema order (outs last), matching the
  generated signature. Recovers ~121 multi-out ops (sort, topk, svd,
  native_batch_norm.out).
- New foreach_out category: materialize + box every TensorList including
  out, call _outf, void return. Recovers 75 _foreach_*.out optimizer ops.
- New vector_return category: split/unbind returning std::vector<Tensor>,
  unboxed via UnboxTensorVecToFlagos.

Remove the multi_out skip in enumerate_all_cuda_ops and the corresponding
80 entries from codegen_skip_ops.txt. Rename local 'result' to '_ret' in
the tuple out-variant branch to avoid collision with out-params named
'result'. Fix with_cuda_libtorch.sh to add .libtorch_cuda_assets to
LD_LIBRARY_PATH so SVD's lazy dlopen of libtorch_cuda_linalg.so resolves.

Generates 2024 ops (up from ~1900 effective). Regression green: ops 311
passed, full CUDA coverage 46, core+Qwen3 infer/train 124 passed.

* feat(cuda): recover 19 more ops — function-only inplace activations + TensorList-out

gen_inplace now branches on torchgen variants: function-only inplace ops
(silu_/gelu_/celu_/leaky_relu_/threshold_/hardtanh_/mish_/hardsigmoid_/
hardswish_/elu_/embedding_renorm_/rrelu_with_noise_) call the free function
at::op_(self,...) instead of the nonexistent Tensor method self.op_().

gen_foreach_out now handles a single mutable Tensor& out with return
(cat.out/stack.out/_stack.out/block_diag.out/_chunk_cat.out) and boxes every
mutable Tensor& (not just the out list), branching return shape on ret_type.
Tuple-returning multi-out RNN out-variants (_cudnn_rnn.out/_lstm_mps.out/
miopen_rnn.out) added to skip list — they were latent -Wreturn-type bugs.

2043 ops generated (was 2024). Regression: ops 311 passed, coverage 46,
core+Qwen3 124 — no regressions.

* feat(ascend): finish torch-2.11 codegen migration for Ascend backend

The torch-2.11 migration (285f52c) unified per-op headers into
generated/ops.h but only adapted the CUDA backend, leaving the Ascend
backend on the deleted per-op-header layout so it no longer compiled.

- Point all ascend + flagos python_wrapper kernels at generated/ops.h
- Rename _softmax / sum.dim_IntList dispatchers to the codegen names
  (PrivSoftmaxFn/priv_softmax_dispatcher, SumDimIntlistFn/sum_dim_intlist_dispatcher)
- Split mm/bmm into functional + out variants over a shared aclnn helper
  to match the new MmFn/MmOutFn (BmmFn/BmmOutFn) signatures
- Implement AscendDeviceMemory (ACL-backed DeviceMemoryInterface) so the
  flagos block-pool allocator works on Ascend; previously GetCachingAllocator
  hit TORCH_CHECK(false) and .to("flagos:0") threw before any kernel ran

Verified on Ascend 910 (torch 2.11.0+cpu, CANN): build+install+import OK;
mm/bmm/add/mul/cos/sin/neg/abs/silu/rsqrt/softmax/sum.dim/mean.dim match CPU.

* feat(ascend): aclnn codegen MVP — unary category

Add a category-based aclnn codegen for the Ascend backend. Unlike the CUDA
codegen (which emits one-line at::op boxing bodies and lets PyTorch marshal
everything), aclnn needs per-op knowledge the aten schema does not carry
(API name, arg marshaling, output allocation), so generation is driven by
category templates + an aten->aclnn mapping table.

- scripts/codegen_ascend.py: reuses codegen_ops.py:schema_to_cpp_name so
  symbol names match the dispatcher decls already in generated/ops.h; only
  fills the Backend::kAscend slot. Validates each aclnn symbol via nm on
  libopapi.so before emitting; skips unmapped/missing ops with a warning.
- csrc/aten/backends/ascend/generated/ascend_kernels.cc: 8 unary ops
  (sqrt/exp/tanh/sigmoid/reciprocal/log/floor/ceil). Path auto-excluded from
  non-ascend builds by the existing CMake glob.
- backends_ascend.conf: route the 8 new ops to ascend.
- docs: design (ascend_aclnn_codegen.md), NPU plan (ascend_npu_plan.md),
  route-rejection record (cpu_torch_external_libtorch_npu.md), and the
  standalone feasibility prototype.

Verified on Ascend 910: all 8 ops match CPU reference, max_err <= 1.2e-7.

* feat(ascend): generalize aclnn codegen to 6 categories (51 ops)

Extend the category-based aclnn codegen from unary-only to six categories,
driven by a CATEGORIES dict (one kernel-body template each) + an OPS dict
mapping each op to (category, aclnn-name override):

- unary (28): erf/erfc/expm1/log2/log10/log1p/round/trunc/frac/sign/relu/
  cosh/sinh/asin/atan/asinh/acosh/atanh/logical_not/bitwise_not + the 8 from P1
- binary (7): div.Tensor/pow.Tensor_Tensor/atan2/maximum/minimum/
  bitwise_or.Tensor/bitwise_xor.Tensor
- binary_alpha (1): sub.Tensor
- binary_cmp (7, bool out): eq/ne/gt/lt/ge.Tensor + logical_and/logical_or
- binary_scalar_alpha (2): add.Scalar/sub.Scalar
- binary_scalar_cmp (6, bool out): eq/ne/gt/lt/ge/le.Scalar

Device-coercion fix in the shared binary prologue: torch.sub(x, 3.0) and
similar tensor-op-python-scalar forms lower to aten::<op>.Tensor (not .Scalar)
with the scalar packed as a CPU scalar tensor. The prologue now coerces the
other operand to self's device (other.to(self.options()) when not already on
the flagos device), mirroring the handwritten add.cc; coercing dtype alone
left CPU storage to be read as an NPU device address, producing all-nan
output. Both operands are expanded+materialized to the broadcast shape since
aclnn does not always broadcast.

Symbol validation via nm on libopapi.so auto-excludes ops without the aclnn
symbol or dispatcher (square/isnan/isfinite).

Verified on Ascend 910: all 51 ops match CPU reference (unary max_err <= 4.4e-5,
binary <= 4.7e-6, comparisons exact).

* feat(ascend): add reduce categories to aclnn codegen (55 ops)

Add three reduce-shaped categories to the aclnn codegen, taking the
generated Ascend kernel set from 51 to 55 ops:

  reduce_dims      amax/amin -- (Tensor, IntArrayRef dim, bool keepdim),
                   same dtype. Reuses the handwritten sum.cc dim logic:
                   wrap negative dims, empty list = reduce all, drop (or
                   set to 1 with keepdim) each reduced dim high-to-low.
  reduce_dim_bool  any.dim -- single int64 dim, bool out. aclnnAny takes a
                   dim list, so the dim is wrapped into a one-element vec.
  cumsum           (Tensor, int64 dim, optional dtype) -- same-shape scan.

Reduce ops are heterogeneous (each aclnn reduce has its own arg layout),
so there is no single "reduce" template; each sub-shape is its own
category. The long tail (max.dim/min.dim tuple return, var/std/norm with
correction/p args, argmax/argmin/prod/logsumexp with no aclnn symbol in
this CANN) is left for later, bespoke handling.

All 4 new ops verified on Ascend 910 vs CPU reference across single-dim,
dim-list, negative-dim, all-reduce, and keepdim variants (8/8 subtests).

* feat(ascend): expand aclnn codegen to 20 categories (77 ops)

Add 11 new categories / 22 ops to the Ascend aclnn generator:
- unary_bool (isinf), unary_scalar (leaky_relu/clamp_min/clamp_max/
  fmod.Scalar), unary_two_scalar (softplus/threshold), unary_int
  (tril/triu), unary_dims (flip)
- addcmul/addcdiv (3-tensor broadcast + Scalar value)
- pow_scalar_tensor (pow.Scalar: Scalar self, Tensor exponent)
- reduce_max_dim (max.dim/min.dim: tuple(values, int64 indices))
- cumprod (separate from cumsum: aclnnCumprod takes dim as aclScalar*)
- act_backward (tanh_backward/sigmoid_backward), threshold_backward
  -- first training-oriented backward ops
- grow binary (fmod.Tensor/floor_divide) and binary_cmp (logical_xor)

All 22 verified on Ascend 910 vs CPU reference. Candidates were probed
against both the dispatcher decls in generated/ops.h and the aclnn
symbols in libopapi.so before templating; ops with no symbol or bespoke
args (var/std/norm, argmax, gelu string_view, matmul family) are left
long-tail and auto-skipped by the generator's nm validation.

* feat(ascend): main-line aclnn codegen batch — activations/loss/gemm (91 ops)

Extend the category codegen toward the training/inference main line: +14 ops
across 9 new/grown categories, all verified on Ascend 910 vs CPU reference.

New categories:
- elu (alpha/scale/input_scale)
- loss (mse_loss; reduction None=elementwise, Mean/Sum=scalar)
- cummax_cummin (tuple values+int64 indices, same shape)
- aminmax (tuple min+max, optional dim)
- prod (scalar out, optional dtype)
- gemm_addmm / gemm_baddbmm (beta/alpha + cubeMathType)
- mv / dot

Grown: unary_scalar += celu/softshrink/hardshrink;
       unary_two_scalar += hardtanh.

smooth_l1_loss is intentionally left long-tail: its aclnn signature takes a
by-value float beta, and EXEC_ASCEND_CMD marshals args through a fully
variadic function-pointer typedef, which is unsafe for a bare float on
aarch64 (beta arrived as 0 -> pure L1 output). Scalars wrapped as aclScalar*
or int64 are varargs-safe; a raw float is not.

* feat: codegen-ize FlagGems Python path — auto-discover 235 ops (5 → 235)

Replace the hardcoded 5-entry FLAGGEMS_PYTHON_MAP with automatic discovery
from flag_gems._FULL_CONFIG (433 ops) plus safety filtering, and generate
per-category kFlagOsPython kernels through a schema-driven generic IValue caller.

Discovery + safety gates (scripts/codegen_ops.py):
- arity gate (hard red line): exclude ops where the flag_gems positional
  count differs from the aten arg count (gems silently drops trailing scalar
  args like add.Tensor's alpha / addcmul's value → wrong results).
- type gate: every arg type must be in the generic caller's supported set;
  ScalarType excluded (IValue stores it as a plain int, indistinguishable).
- categories: functional_pure / inplace / tuple_return / out_variant.
- FLAGGEMS_PYTHON_SKIP holds 8 convergence holdouts: mm.out (required out
  kwarg the generic caller can't supply) and 7 ops with an unconditional
  device.type=="cuda" assert flagos PrivateUse1 tensors can't satisfy
  (maximum, minimum, 5 upsample variants).

Result: 150 functional_pure + 57 inplace + 21 tuple_return + 7 out_variant
= 235 ops routed to flagos_python. backends_flaggems.conf now auto-generated.

Generic caller (python_op_caller.{h,cc}): IValueToPython covers Tensor/int/
double/bool/None/str/Scalar/IntList/DoubleList/BoolList/TensorList;
CallPythonOp_Generic uses BuildPyArgs; add CallPythonOp_GenericTuple for
tuple-returning ops; GetFunc resolves dotted module.func qualnames.

Cleanup: delete stale hand-written flagos wrappers superseded by codegen.

Verification: numerical spot-check 23/23 (err <= 3e-5); flaggems_python
correctness 27 passed; CUDA-direct path 330 passed / 45 skipped / 3 xpassed
(matches baseline, no degradation).

* feat(ascend): add layer_norm/group_norm to aclnn codegen (93 ops)

* feat(flaggems): recover 18 out-variants where gems takes out positionally (235 -> 253)

The out_variant safety gate assumed flag_gems never accepts the aten `out`
tensor, so ops whose gems function signature is (…non_out, out) failed the
arity check (npos == non_out+out, not == non_out) and were excluded.

Add an out_variant_gemsout category: when gems npos == #non_out + #out args,
pass the aten out tensor(s) positionally to gems (which writes into them in
place) and return the aten out arg. Distinguished from mm.out, whose gems out
is a required keyword-only arg the positional caller can't supply (stays in
FLAGGEMS_PYTHON_SKIP).

Recovers: atan2/bmm/cosh/div/exp/expm1/fmin/hardsigmoid/i0/log10/logaddexp/
pixel_unshuffle/reflection_pad1d/reflection_pad2d/replication_pad1d/softshrink/
special_i0e/where.self .out variants.

Verified: 10/10 numerical spot-checks via dispatcher, flaggems_python 27 passed,
CUDA-direct 330 passed / 45 skipped / 3 xpassed (no degradation).

* feat(ascend): add gelu/log_softmax/softmax-backward to aclnn codegen (98 ops)

Add 4 backbone categories (6 ops), all verified on Ascend 910 vs CPU:
- gelu / gelu_backward: use aclnnGeluV2 (int64 approximate) + aclnnGeluBackwardV2
  (char* approximate). v1 aclnnGelu hardcodes the tanh approximation, but
  PyTorch's default is approximate="none" (erf form, used by qwen3 et al);
  V2 selects 0="none"/1="tanh" so both modes match (err 1.8e-07 / 1.2e-07).
- _log_softmax: mirrors handwritten softmax.cc (aclnnLogSoftmax(self,dim,out)).
- _softmax_backward_data / _log_softmax_backward_data: aclnnSoftmaxBackward /
  aclnnLogSoftmaxBackward (grad_output, output, dim, grad_input), out dtype =
  input_dtype.

* feat(flaggems): recover 4 ops with trailing-default gems params (253 -> 257)

gems funcs addcdiv/round/scatter/scatter_ have extra positional params
with defaults beyond the aten args (out=None, decimals=0, reduce=None).
Relax the arity gate to admit npos > ncall when every extra gems param
is strictly trailing with a default, guarded against the reordering trap
(gems gather inserts out=None mid-signature -> aten sparse_grad would be
misrouted into the out slot; correctly excluded).

* feat(ascend): add addmv/addr + BCE loss family to aclnn codegen (103 ops)

Add 5 ops across 5 categories, all verified on Ascend 910 vs CPU:
- addmv / addr: gemm-family completion. aclnnAddmv arg order is (self,mat,vec,
  alpha,beta) -- alpha before beta, opposite of addmm. addr has no cubeMathType.
- binary_cross_entropy (+optional weight), _backward, and _with_logits
  (+optional pos_weight). Optional tensors marshal via value_or(Tensor()) ->
  AclTensorWrapper nullptr, which aclnn treats as absent.

Left out (probed but not shipped): addbmm (hf32 cube accumulation over the batch
dim inflates rel-err to ~1e-2 vs ~1e-4 for a single addmm) and native_batch_norm
(aclnnBatchNorm returns ACLNN_ERR_INNER_NULLPTR on 4D NCHW input; 2D N,C works).
Both deferred to the conv/pool bespoke batch.

* refactor(ascend): migrate 19 handwritten seed kernels to aclnn codegen

Pre-codegen "seed" kernels (abs/cos/add.Tensor/mul.Scalar/where/softmax/sum/
mean etc.) had bodies expressible by codegen categories -- several byte-identical
to the templates. Migrate them into scripts/codegen_ascend.py and delete the
handwritten .cc files, per the rule: anything a codegen category can express
goes through codegen; handwrite only genuine bespoke ops.

- Reuse existing categories: abs/acos/cos/sin/neg/rsqrt/silu (unary),
  mul.Tensor/bitwise_and.Tensor (binary), add.Tensor (binary_alpha),
  pow.Tensor_Scalar (unary_scalar).
- Add 7 new categories: binary_scalar (mul.Scalar->aclnnMuls, div.Scalar->
  aclnnDivs -- headers absent, marshaling from handwritten refs),
  act_backward_self (silu_backward: grad+self), where (aclnnSWhere 3-tensor),
  softmax_fwd (_softmax, half_to_float), reduce_all (all), reduce_sum_dtype
  (sum.dim_IntList), reduce_mean_dtype (mean.dim via aclnnMeanV2).

Kept handwritten (SKIP={le.Tensor,mm,bmm}): le (aclnnLe absent, needs runtime
version probe), mm/bmm (also register out-variants codegen doesn't emit),
factories, TensorList/SymInt ops, embedding, nll_loss.

Handwritten kAscend regs 36->17, codegen 103->122, total unchanged. Verified
all 19 migrated ops vs CPU on Ascend 910 incl. CPU-scalar coercion,
half_to_float, dtype promotion, broadcast -- zero numeric regression.

* feat(metax): reuse CUDA boxing kernels on MetaX via FLAGOS_METAX_BOXING

Add a boxing build mode for the MetaX backend that reuses the generated
CUDA boxing kernels (csrc/aten/generated/cuda_kernels.cc, host g++, no
mxcc/nvcc) which dispatch PrivateUse1 -> CUDA into maca's libtorch_cuda.so,
instead of the hand-written mxcc .cu kernels under backends/metax/.

- CMakeLists.txt / setup.py: FLAGOS_METAX_BOXING=1 sets METAX_KERNEL OFF
  while keeping the MetaX SDK runtime + cu-bridge headers path.
- csrc/CMakeLists.txt, torch_fl/csrc/CMakeLists.txt: define USE_MACA=1 for
  metax, required because maca's torch headers are a hard fork gated on it
  (C10_WARP_SIZE, Context.h allow_tf32_cudnn hit static_assert(0) otherwise).
- Regenerate generated/* + backends_cuda.conf against maca 2.10 torch
  (2035 ops; +cudnn_convolution_bias_fused[.out], -upstream-only ldexp/
  _foreach_powsum/miopen_ctc_loss/_flash_attention_forward.quantized).

Verified on 8x MetaX C550: import + factory ops + add/mul/relu/softmax/
sum/exp/mm/bmm/copy all match CPU reference (matmul matches native
maca-cuda exactly; ~1e-3 vs CPU is MetaX TF32 precision).

* feat(ascend): aclnn codegen for conv/pool family (5 categories)

Add convolution (fwd+bwd) and 2D pooling to the aclnn codegen. These are the
first ops needing an explicit output-shape formula since aclnn requires the
output pre-allocated -- each template carries a small shape helper.

Categories:
- adaptive_avg_pool2d, avg_pool2d, max_pool2d_with_indices (tuple w/ int64 idx)
- convolution (non-transposed), convolution_backward (3-tuple + output_mask)

Two infra additions in op_api_common.h:
- AclTensorWrapper gains an optional aclFormat param (default ND). avg_pool2d/
  adaptive_avg_pool2d/convolution reject ND 4-D tensors (GetWorkspaceSize ret
  161002); they need NCHW/NCL/NCDHW per rank. max_pool2d does NOT care -- the
  requirement is per-aclnn, so the param defaults to ND and is opt-in.
- AclBoolArrayWrapper for convolution_backward's output_mask[3].

conv cubeMathType=0 (KEEP_DTYPE); type 1 loses ~2.5e-3 on the cube unit.

Verified vs CPU on Ascend 910: conv fwd/bwd err~1e-6 incl stride/padding/
dilation/grouped; pooling exact-to-1e-7 incl ceil_mode and stride defaults;
prior migrated ops regression-clean. codegen 122->127 ops, 52 categories.

* feat(flaggems): forward keyword-only gems args, recover 31 ops (257 -> 288)

Many gems funcs declare aten's trailing args keyword-only (sum(inp,*,dtype),
add(A,B,*,alpha), gelu(self,*,approximate), var(x,dim,*,correction,keepdim)).
The positional-only generic caller couldn't pass them, so non-default values
were silently dropped -> these were wrongly excluded as arity_short.

Add CallPythonOp_GenericKw/GenericKwTuple: forward the trailing aten args by
NAME via a PyKwarg vector. dtype (ScalarType) is tagged is_dtype so the caller
converts the int payload to a torch.dtype (IValue can't distinguish ScalarType
from int); absent optionals set is_none -> Python None. Discovery matches each
trailing aten arg to a gems keyword-only param by name (reject on mismatch) and
gates types via _FLAGGEMS_KWARG_OK.

Recovers 31 ops incl add/sub/addmm/addmv/addr/addcmul (alpha/beta/value),
sum/mean/prod/cumsum (dtype), gelu (approximate), var/std/var_mean (correction),
isin (invert), sort.stable. Spot-checked all kwarg types numerically (err<=1e-5,
non-default alpha/dtype/correction now correctly applied). Blocked: Generator?/
Layout?/Device? args and name-mismatches (multinomial/var.dim/_grouped_mm).

* feat(ascend): aclnn codegen for max_pool bwd + batch_norm (CNN train loop)

Complete the CNN training closure on top of conv/pool: add max_pool2d
backward, native_batch_norm forward and backward. conv + pool + bn now
cover a full CNN forward/backward pass.

Categories:
- max_pool2d_with_indices_backward -> grad_input
- native_batch_norm -> (out, save_mean, save_invstd)
- native_batch_norm_backward -> (grad_input, grad_weight, grad_bias)

Two per-op quirks (each read from the aclnn header @param notes):
- max_pool2d FWD ignores format and emits int64 indices, but the BWD kernel
  requires NCHW format AND int32 indices -- fwd/bwd are not symmetric. The
  bwd template casts indices to int32 and tags NCHW.
- batch_norm's save_invstd uses a different definition than PyTorch CPU
  (~0.18 apart), but this does not affect training: the backward consumes the
  same NPU save_invstd and all three grads match CPU to <=4e-6. running_mean/
  var are passed non-const (updated in-place). This overturns the earlier
  "native_batch_norm left out (561103)" note -- that was marshaling, not a
  real limitation.

Verified vs CPU on Ascend 910: max_pool bwd 1e-7; bn fwd out 2e-7, bn bwd
grads <=4e-6, bn eval exact; conv/pool/elementwise regression-clean.
codegen 127->130 ops, 55 categories.

* feat(ascend): aclnn codegen for pool/norm backward (fwd+bwd complete)

Round out the backward coverage so conv, avg/adaptive pool, layer_norm and
group_norm all have both forward and backward -- covers the remaining norm/
pool grads for CNN and transformer training.

Categories:
- avg_pool2d_backward, _adaptive_avg_pool2d_backward -> grad_input
- native_layer_norm_backward -> (grad_input, grad_weight, grad_bias)
- native_group_norm_backward -> (grad_input, grad_gamma, grad_beta)

Notes:
- avg_pool2d/adaptive_avg_pool2d backward also require NCHW format, matching
  their forwards.
- norm backwards feed the forward's mean/rstd straight through and are self-
  consistent (no save_invstd-style mismatch: layer/group norm expose rstd,
  not invstd).
- aclnn names: native_layer_norm_backward maps to aclnnLayerNormBackward
  (aclnnNativeLayerNormBackward does not exist); group_norm likewise.

Verified vs CPU on Ascend 910, all <=1e-6 (avg_pool bwd 3e-8, adaptive exact,
ln/gn grads <=1e-6); conv/max_pool/elementwise regression-clean.
codegen 130->134 ops, 59 categories.

* feat(flaggems): promote ScalarType positional args to dtype kwargs, +3 ops (288 -> 291)

The positional caller can't carry a ScalarType (IValue stores it as a plain
int), so ops taking dtype/input_dtype as a positional aten arg were excluded by
the type gate (type_gate_dtype: 4 ops). Now that the kwarg path tags is_dtype
and converts to torch.dtype by name, promote any ScalarType positional arg into
a by-name kwarg -- safe when gems accepts it by name (not positional-only) and
the ScalarType args form a strict suffix (guard rejects middle-dtype reordering).

Recovers _softmax_backward_data, _log_softmax_backward_data (input_dtype),
linalg_vector_norm (dtype). Spot-checked: backward err<=2e-7, vector_norm f32
ord=1/2 correct. _safe_softmax -> SKIP (gems device assert rejects PrivateUse1,
same class as maximum/minimum). vector_norm dtype=f64 raises gems' own
NotImplementedError (gems limitation, not a forwarding bug -- f32 path correct).

Regressions green: cuda-direct 330 passed, flaggems_python 27 passed.

* feat(ascend): aclnn codegen for masked_fill/gather/index_select

Fill the remaining Transformer op gaps. A coverage probe showed embedding,
gelu, silu, bmm, baddbmm and softmax were already handled (handwritten or
earlier codegen), so the real gaps were masking and indexing.

Categories:
- masked_fill.Scalar, masked_fill.Tensor -> broadcast(self, mask)
- gather -> index shape; index_select -> self shape w/ dim -> index.numel()

Note on out-of-place via inplace aclnn: aclnn only ships inplace masked_fill
(aclnnInplaceMaskedFillScalar/Tensor). Implementing the out-of-place aten op
means copy-then-fill, but self.clone() routes through empty_like which is NOT
registered for the ascend backend. Allocate via apply_tensor_without_format +
out.copy_(self.expand(...)) instead -- applies to any copy-then-mutate kernel.

SDPA/flash-attention is fused with a bespoke multi-tensor signature and is
left to a dedicated batch.

Verified vs CPU on Ascend 910: all exact (err=0) incl broadcast mask, 3D
gather, negative dims, vocab-size index_select; core ops regression-clean.
codegen 134->138 ops, 63 categories.

* feat(flaggems): codegen factory ops (arange/eye/full/ones/zeros/linspace/logspace), +10 (291 -> 301)

Factory ops don't take input tensors -- gems generates the tensor itself. New
CallPythonOp_Factory injects device=flagos (so gems' internal torch.empty hits
OUR allocator -> PrivateUse1 tensor, no CUDA round-trip, no recursion),
layout=strided (gems eye/randperm reject layout=None), pin_memory=None, and
forwards the aten dtype. discover_flaggems_ops gains a factory branch that
strips the TensorOptions fields and passes only shape/scalar positionals;
requires gems to accept dtype/layout/device by name (kwonly on every factory).

Recovers 10 ops: arange (+.start/.start_step), eye (+.m), full, linspace,
logspace, ones, zeros. Spot-checked all exact (err=0) vs CPU incl f64 dtype.

Two correctness guards:
- arange: gems defaults dtype=None to int64 unconditionally, but aten infers
  float when any of start/end/step is floating -> arange(0.,3.,.5) was silently
  wrong ([0,0,1,1,2,2]). Kernel now replicates aten's rule and passes an
  explicit dtype. Verified arange float now exact.
- rand/randn/randperm SKIP: gems reaches default_generators[device], but the
  PrivateUse1 device has none (IndexError); randperm asserts an int dtype. Same
  root cause as the Generator? blocked group -- can't express per-device gen.

Regressions green: cuda-direct 330 passed, flaggems_python 27 passed.

* docs(ascend): record SDPA/flash-attention research findings

- Investigated aclnnFlashAttentionScore/Grad API semantics
- Confirmed: inputLayout="BNSD", softmaxMax/Sum shape [B,N,S,8] float32
- Confirmed: causal (sparseMode=3, preTokens=INT32_MAX, nextTokens=0) vs
  full (sparseMode=0, preTokens=65536, nextTokens=65536)
- Key blocker: PyTorch logsumexp [B,N,S] vs aclnn (softmaxMax, softmaxSum)
  [B,N,S,8] shape mismatch; logsumexp=log(Sum)+Max but 8-way tiling
  collapse unclear
- Conclusion: SDPA is bespoke multi-day task (autograd Function wrapper,
  ctx save/restore, causal/dropout/mask testing), deferred to dedicated pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ascend): implement SDPA forward via aclnnFlashAttentionScore

Add handwritten kernel for _scaled_dot_product_efficient_attention (forward only).
Wraps aclnnFlashAttentionScore with BNSD layout, handles logsumexp mapping from
aclnn's softmaxMax+softmaxSum [B,N,S,8] to PyTorch's [B,N,S] via [:,:,:,0] indexing.

Key findings:
- attenMask semantics: true=MASK_OUT (opposite of docs), false=KEEP
- Causal attention: triu(ones, diagonal=1) masks future positions
- Verified: non-causal err=3.34e-06, causal err=7.15e-07 vs CPU

Backward NOT implemented: aclnnFlashAttentionScoreGrad needs separate softmaxMax
and softmaxSum, but PyTorch's autograd only saves single logsumexp (log addition
not invertible). Forward-only covers inference; training needs architectural work.

Files:
- csrc/aten/backends/ascend/scaled_dot_product_attention.cc (new, 120 lines)
- csrc/CMakeLists.txt (add to ascend sources)
- test_sdpa_ascend.py (verification script)
- docs/ascend_aclnn_codegen.md (document SDPA + attenMask/logsumexp pitfalls)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ascend): implement SDPA forward and backward kernels

- Add _scaled_dot_product_efficient_attention (forward)
- Add _scaled_dot_product_efficient_attention_backward (backward)
- Wrap aclnnFlashAttentionScore and aclnnFlashAttentionScoreGrad
- Implement activation checkpointing for backward (recompute to get softmaxMax/Sum)
- Support causal and non-causal attention
- Add dropout=0 constraint (aclnn dropout needs explicit mask handling)
- Tests pass: forward+backward, causal mask scenarios

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single self-contained wheel with runtime flaggems switch

Move the flaggems-vs-cuda choice from compile time + external LD_PRELOAD
to a pure runtime env switch, so one wheel serves both paths.

- Default FLAGGEMS_PYTHON option ON: compile both CUDA boxing kernels and
  flaggems_python kernels into libtorch_fl.so unconditionally (MetaX keeps
  it opt-in). No more FLAGGEMS_PYTHON=ON at build time.
- Bundle .libtorch_cuda_assets/*.so* into torch_fl/lib and ctypes-preload
  them (nvidia deps -> torch cpu libs -> cuda libs) before import torch, to
  satisfy the CUDAHooks hard constraint. FLAGOS_DISABLE_CUDA_ASSETS skips.
- FLAGOS_USE_FLAGGEMS selects backends_flaggems.conf vs backends_cuda.conf
  at import; FLAGOS_OP_<name> per-op overrides still apply. Both confs and
  the nvidia-*-cu12 runtime deps (CUDA builds) are now packaged.

Verified on a clean shell (no wrapper, auto-preload): cuda-path ops
330 passed/45 skipped/3 xpassed; FLAGOS_USE_FLAGGEMS=1 flaggems_python
27 passed; all three switch levels confirmed.

* feat(flaggems): route random in-place + *_like factory ops, +6 (301 -> 307)

Add two codegen paths to the FlagGems Python bridge:

- *_like factory (zeros_like/ones_like/full_like): new CallPythonOp_LikeFactory
  injects device=flagos/layout=strided/memory_format=None/pin_memory=None and
  forwards dtype; gems reads shape/device from the source tensor positional.
- random in-place (uniform_/exponential_/bernoulli_.float): new
  CallPythonOp_RandomInplace injects a module-level CUDA torch.Generator as the
  generator kwarg. gems only reads philox seed+offset from it (randoms computed
  in the Triton kernel writing into the flagos tensor), so the generator device
  need not match. A CUDA generator is required: a flagos/CPU generator's
  5056-byte MT19937 state fails gems' 16-byte CUDA-philox state unpack. Serialized
  by the GIL; offset advances per call via gems' set_state.

discover_flaggems_ops gains like_factory / random_inplace branches; random ops
are whitelisted (_FLAGGEMS_RANDOM_INPLACE) since normal_ is also inplace+Generator?
but hardcodes generator=None internally and can't be routed.

Explicitly skipped with reasons: normal_/normal.* (gems drops the generator),
rand/randn/randperm/rand_like/randn_like (no generator param -> empty PrivateUse1
default_generators), multinomial (name-mismatch + generator).

Also add docs/flaggems_no_dispatcher_analysis.md: the 88 no-dispatcher ops all
already execute correctly (composite_implicit decomposition / fallback / manual
registration), so they are not a functional gap and bulk routing would drop
autograd.

Validated: like ops err=0 incl dtype override; random ops distribution-correct
with distinct streams across calls. Regression unchanged (cuda 330/45/3,
flaggems_python 27).

* feat(metax): auto-symlink stock torch/lib to maca C++ runtime at import

Reuse PyTorch's CUDA boxing kernels on MetaX with a stock torch==X.Y.Z+cpu
wheel by symlinking the active wheel's torch/lib core .so to the MetaX torch
wheel's copies, so the process loads the MetaX C++ runtime (at::maca::* fork)
instead of the upstream one.

A runtime hook in torch_fl/__init__.py runs ensure_maca_libtorch_links() BEFORE
import torch (afterwards libc10 is already mapped and relinking is too late).
Pure ctypes preloading does not work: the official _C.so / libtorch_python.so
carry an $ORIGIN RUNPATH that pulls upstream libc10 back in by full path,
double-loading it and crashing on duplicate caffe2 static init. Symlinking makes
the physical files RUNPATH resolves to be the MetaX ones.

Gated on FLAGOS_METAX_BOXING=1; idempotent; backs up originals to
torch/lib/_orig_backup/ (restore_original_libtorch() reverts); no-op when the
active torch already IS the MetaX wheel. MetaX torch/lib discovered via
FLAGOS_MACA_TORCH_LIB env or conda-env scan.

Verified on torch 2.10.0+cpu: full_cuda_coverage 45/46 (the 1 fail is addmm TF32
rounding), per-op suite 310 passed / 45 skipped / 3 xpassed (8 fails are all
flaggems-backend or test-case issues, not op wiring).

* chore(codegen): regen generated ops for torch 2.10 schema

Drop cudnn_convolution_bias_fused (2.11-only op absent in 2.10). Regen
matches the 2.10 aten schema; flaggems_python and CUDA paths verified
against the 2.11 baseline (27 passed; 330 passed/45 skipped/3 xpassed).

* feat(metax): bundle forked libtorch into self-contained wheel

Package the MetaX-forked libtorch C++ .so (~1.1G) inside the wheel under
torch_fl/lib_maca/ so target machines need only the official torch+cpu
wheel plus this wheel plus the /opt/maca driver runtime -- no separate
MetaX torch wheel required.

- setup.py: package_data bundles lib_maca/*.so* and all backends*.conf
  (boxing modes select backends_cuda.conf via FLAGOS_BACKEND_CONFIG);
  version gains a +metax local segment (FLAGOS_WHEEL_LOCAL overridable).
- pyproject.toml: mark version dynamic so setup.py's computed +metax tag
  is not overridden by a static [project] version.
- _metax_libtorch_link.py: _discover_maca_torch_lib prefers the bundled
  lib_maca/ over env var / conda scan; symlinks the stock wheel's
  torch/lib to the bundled forked libtorch at import.
- scripts/bundle_maca_libtorch.sh: copy the 8 forked libtorch .so and
  patchelf their RPATH to $ORIGIN + /opt/maca for the target runtime.
- .gitignore: ignore torch_fl/lib_maca/ (build artifact).

Verified end-to-end from a fresh wheel install in a clean env (official
torch 2.10.0+cpu, no MetaX torch wheel, no LD_LIBRARY_PATH): flagos
compute on MetaX GPU works, torch/lib auto-symlinks to the installed
lib_maca, libmcblas loads from /opt/maca; op coverage 45/46 (only addmm
TF32 rounding fails).

* docs(metax): document self-contained boxing wheel packaging and usage

Add a MetaX Self-Contained Wheel (CUDA boxing) section covering the
FLAGOS_METAX_BOXING=1 path: how to build the wheel (bdist_wheel +
bundle_maca_libtorch.sh + repackage), the ~1.1G size / distribution
tradeoff, and how to install and run on a clean target (official
torch+cpu + this wheel + /opt/maca, no torch+metax wheel, no manual
LD_LIBRARY_PATH). Note the two MetaX build modes in the runtime notes
and warn that FLAGOS_USE_FLAGGEMS=1 must not be used with the boxing
wheel (flagos_python backend is not compiled -> backend not registered).

* docs(metax): add MetaX developer portal link for SDK and torch+metax wheel

Point readers to https://developer.metax-tech.com/softnova (SoftNova) for
the MACA SDK (driver + cu-bridge + mxcc/cucc) and the torch+metax wheel,
noting login is required and versions must match the card/driver/Python.
Referenced from the top-level prerequisites, the MetaX source-build
prerequisites, and the boxing-wheel build step.

* docs(metax): drop mxcc source-build path, keep only the boxing wheel

We no longer use the hand-written mxcc/cucc kernel build (METAX_KERNEL=ON
+ torch+metax + Triton). Remove that 'Build from Source (MetaX Platform)'
section and promote the self-contained CUDA boxing wheel to be the single
MetaX build path (renamed to 'Build from Source (MetaX Platform)').

- Fold the MetaX developer-portal (SoftNova) SDK / torch+metax wheel
  download note into the boxing build steps (needed to build, not run).
- Rewrite the 'Two build modes' runtime note to describe only the boxing
  wheel and fix its now-stale section anchor.

* feat(flaggems): route varargs unary in-place + rng ops, +13 (307 -> 320)

varargs unary in-place (7): asinh_/sinh_/log1p_/digamma_/sgn_/hardswish_/
logit_. gems wraps these as (*args, **kwargs) so inspect.signature can't
recover arity; add _FLAGGEMS_ARITY_OVERRIDE so the aten schema supplies the
authoritative positional count. Whitelist only holds simple elementwise ops
verified to run + match CPU (maxdiff <= 1e-6) and drop no kwarg.

rng (6): rand/randn (factory), rand_like/randn_like (like_factory), randperm
(factory; no generator arg in this torch schema), multinomial (new rng_dropgen
category dropping the trailing Generator?). Unblocked by _patch_flaggems_philox()
in torch_fl/__init__.py, which monkeypatches gems'
philox_backend_seed_offset to fall back to a held CUDA generator when
torch.cuda.default_generators is empty (CPU-torch + cuda shim). One patch covers
all 6 rng ops; no caller C++ change needed.

Excluded: i0_/zero/zero.out hit a hardcoded tensor.is_cuda assert in the gems
kernel (flagos is PrivateUse1, never true) so they stay in FLAGGEMS_PYTHON_SKIP;
normal_/normal.* hardcode generator=None upstream (can't thread our generator).

Verified: 15/15 numeric spot-checks pass; regressions unchanged from baseline
(cuda-path 330 passed/45 skipped/3 xpassed, flaggems_python 27 passed). Fresh
regen also drops the now-stale cudnn_convolution_bias_fused[.out] from
backends_cuda.conf (absent from the torch 2.10 schema).

* feat(ascend): enable high-level F.scaled_dot_product_attention

Route the high-level SDPA API to the aclnnFlashAttentionScore kernel
instead of the math decomposition path, and register the view ops its
pre/post-processing needs.

- Register _fused_sdp_choice_stub DispatchStub for PrivateUse1 returning
  efficient_attention (2). PyTorch's scaled_dot_product_attention selects
  its fused backend via this C++ stub (gated by is_device_supported), not
  the aten-op-level _fused_sdp_choice. Macro must live in namespace
  at::native.
- Add view/metadata ops for kAscend: transpose.int, permute, select.int,
  slice.Tensor, squeeze, squeeze.dim, unsqueeze, _unsafe_view, detach.
  Implemented via at::native::<fn> (not tensor member methods, which
  re-dispatch through PrivateUse1 and recurse -> segfault). select.int
  uses select_symint; explicit _native.h includes avoid int64->Dimname
  overload mis-resolution.

Verified end-to-end on Ascend 910 NPU: forward, causal, autograd
backward all pass; CPU-reference relative error ~0.0004 (fp16).

* feat(ascend): codegen in-place zero_/fill_ via aclnn, +3 (139 -> 142)

Add aclnn codegen for the in-place fill primitives:
- zero_          -> aclnnInplaceZero
- fill_.Scalar   -> aclnnInplaceFillScalar
- fill_.Tensor   -> aclnnInplaceFillTensor

Before this, zero_/fill_ had no device implementation, so the handwritten
factory ops (zeros/ones_like/new_ones/scalar_tensor) fell back to
flaggems/CPU for their internal .zero_()/.fill_() calls -- a hidden h2d
path. Now the whole factory chain runs device-side aclnn. The factory ops
themselves stay handwritten (their device/dtype inference is not
expressible by codegen), but the fill work is pushed down to aclnn.

Also move the SDPA + view-op conf entries above the codegen marker: they
were appended after the "# --- generated by codegen_ascend.py ---" marker,
which codegen truncates on every run, so a regen would silently drop them.
Handwritten conf entries must live before the marker.

Verified on Ascend 910: zero_/fill_.Scalar/fill_.Tensor and all four
factory ops match CPU exactly (diff 0). SDPA + view-op tests still pass.

* feat(ascend): codegen embedding/embedding_backward/constant_pad_nd, +3 (142 -> 145)

Migrate three single-aclnn-call kernels from handwritten to codegen:
- embedding                -> aclnnEmbedding
- embedding_dense_backward -> aclnnEmbeddingDenseBackward
- constant_pad_nd          -> aclnnConstantPadNd

Each was a straight aclnn call with deterministic output-shape logic, so
the handwritten .cc bodies map verbatim into codegen templates. Delete the
three .cc files (globbed by CMake, no explicit list to update) and move
their conf entries from the handwritten section into the generated block.

Handwritten kAscend registrations drop 30 -> 27; codegen 142 -> 145.

Verified on Ascend 910: all three match CPU exactly (diff 0), including
embedding backward via autograd and multi-dim constant padding. Prior
in-place/factory and view-op tests still pass.

* feat(ascend): migrate mm/bmm/cat + factory ops from handwritten to codegen

Extend codegen_ascend.py with three new structural capabilities so more
handwritten kAscend kernels can be expressed as codegen templates:

- .out variants: T_MATMUL / T_MATMUL_OUT generate functional + .out pairs
  (mm/mm.out via aclnnMm, bmm/bmm.out via aclnnBatchMatMul). Reusable pattern
  for any op whose .out kernel writes into a caller-shaped out&.
- TensorList: T_CAT marshals at::ITensorListRef via aclCreateTensorList
  (aclnnCat), filtering numel==0 tensors; does not aclDestroyTensorList.
- factory ops: T_ZEROS/T_SCALAR_TENSOR/T_ONES_LIKE/T_NEW_ONES build
  TensorOptions on-host + at::empty then fill via device-side zero_/fill_.
  New NO_ACLNN_CATEGORIES set skips the libopapi symbol guard for kernels
  that issue no direct aclnn call.

Deletes 7 handwritten .cc (mm/bmm/cat/zeros/scalar_tensor/ones_like/new_ones).
Handwritten kAscend regs 16 -> 7, codegen 145 -> 154, total unchanged.
Verified vs CPU on NPU (test_matmul_codegen.py, test_cat_codegen.py,
test_inplace_fill.py): all pass, matmul f32 within hf32-cube tolerance.

* feat(metax): enable FlagGems on boxing wheel as runtime switch (FLAGOS_USE_FLAGGEMS)

Match the CUDA single-wheel model on MetaX: compile the FlagGems Python path
(flagos_python backend) alongside the CUDA boxing kernels and select between
them purely at runtime via FLAGOS_USE_FLAGGEMS, instead of forcing it off.

- setup.py: drop the metax FLAGGEMS_PYTHON=OFF force; default ON like CUDA
  (only the C++ FlagGems path FLAGGEMS_KERNEL stays off). FLAGGEMS_PYTHON=0
  still available for a slim pure-boxing build.
- torch_fl/__init__.py: add explicit MetaX branch in _patch_flaggems_codegen_config
  (GEMS_VENDOR=metax + patch_torch_cuda_for_metax) before the ascend fallback,
  fixing metax wrongly getting GEMS_VENDOR=ascend; _select_backend_config picks
  backends_metax_flaggems.conf when FLAGOS_USE_FLAGGEMS=1 + FLAGOS_METAX_BOXING=1.
- _metax_compat.py: add stream/availability/manual_seed shims and
  _patch_triton_do_bench (wall-clock) so FlagGems Triton kernels run on the
  CPU-frozen torch wheel against maca libtorch_cuda.so.
- scripts/codegen_ops.py: generate backends_metax_flaggems.conf, routing the ops
  triton-metax cannot run (mm/bmm/mean.dim) and flag_gems device-guarded ops
  (mul, embedding_dense_backward, etc.) back to cuda boxing.
- tests/integration/conftest.py: skip forcing backends_metax.conf in boxing mode
  (mxcc backend not compiled) so torch_fl's own config selection applies.
- tests/integration/ops/conftest.py: skip @mark.metax tests in boxing mode
  (no metax backend to dispatch to).

Verified: 255 passed / 147 skipped; only non-skip failure is the pre-existing
out-of-scope cat empty-1d-tensor boxing bug.

* feat(ascend): enable Qwen3-0.6B generate() end-to-end on real 910

Walks the transformers generate() path op-by-op until inference runs
coherently (~1.8 tok/s) on a pure-aclnn C++ backend, and takes the ops
parity suite from 269 failed/31 passed to 6 failed/294 passed.

Bespoke handwritten kernels (generation path):
- topk/sort/scatter/multinomial via aclnnTopk/Sort/Scatter/Multinomial
- arange/argmax/isin/lift_fresh
- rng.cc: device randn/rand/randint/randint.low via aclnnInplace
  Normal/Uniform/Random, seeded from the default CPU generator's
  random64() so the ops suite's on-device torch.randn(device=DEVICE)
  inputs work.

Codegen (scripts/codegen_ascend.py):
- rsub.Scalar -> binary_scalar_alpha (aclnnRsubs)
- sum/max/min full-reduce -> reduce_sum_all / reduce_minmax_all
- any -> reduce_all; ones/empty_like/full/full_like factory ops

SDPA GQA fix (scaled_dot_product_attention.cc): expand kv heads using
kv's own S_kv, not query's S -- during decode query S==1 but kv S==full
context, so using query S gave an expand size mismatch.

View ops (strided_ops): t, unbind.int (pure metadata, via at::native::).

Runtime config: torch_fl auto-selects the ascend conf on a /dev/davinci*
box; FLAGOS_USE_FLAGGEMS=1 opts into the FlagGems Triton path. Also patch
triton-ascend npu_utils.cpp for the CANN 9.0.0 rtLimitType_t enum name.

* perf(ascend): reach 0.89x torch_npu on Qwen3-0.6B inference

Host-side dispatch optimizations for eager decode on real 910, keeping the
op set identical to torch_npu (no fusion in the measured path).

- aten::empty fast path: skip the DeviceGuard registry round-trip when the
  device is unchanged; drop the ptr_to_block_ side map + its mutex from the
  caching allocator by stashing Block* in the DataPtr context (5.25 -> 2.04
  us/call, the single biggest win)
- repeatable aclOpExecutor cache in op_api_common.h (ExecAscendCached):
  owns its aclTensors, rebinds addresses on hit, reuses the workspace tensor
- codegen: cached categories for elementwise/unary-scalar/reductions/softmax
  plus a CPU-scalar fast path routing T+float through aclnnAdds/Muls/... to
  avoid a per-call H2D copy
- register aten::matmul on AutogradPrivateUse1 -> aclnnMatmul, collapsing
  mm/bmm/view churn and matching torch_npu's op counts exactly
- isin: compute on device instead of a triple CPU round-trip (this was the
  hottest op in the generate() loop)
- _to_copy: cache the aclnnCast executor

Inference 13.4 -> 24.82 tok/s (0.89x torch_npu); training 507.9 tok/s (0.69x).
Adds tests/perf/e2e_qwen3_{infer,train}_ascend.py as the comparison harness.

* fix(ascend): do not declare a triton runtime dep on ACCELERATOR=ascend

Upstream #22 made flag_gems + triton>=3.5.1 hard runtime deps for every
accelerator except dcu. On Ascend the `triton` module is supplied by
triton-ascend, which is installed out of band and has no PyPI release
satisfying triton>=3.5.1, so `pip install -e .` pulls stock triton over it.
Every Triton entry point then fails with "0 active drivers" and the
torch_npu-shim patch from scripts/patch_triton_ascend.py is gone.

Ascend needs the same carve-out as dcu, for the same reason: the accelerator
provides its own triton and PyPI's NVIDIA-targeted wheel is the wrong
artifact. flag_gems imports in the Python layer are ImportError-guarded, so
omitting the dep is safe.

* feat(ascend): _foreach_* kernels for AdamW foreach=True, chunked under aclnn's 50-entry cap

Adds the 7 _foreach_* Ascend kernels the AdamW foreach path needs
(_foreach_mul_/add_.Scalar, _foreach_lerp_.Scalar, _foreach_addcmul_.Scalar,
_foreach_sqrt, _foreach_div_/addcdiv_.ScalarList), plus stack, mean, clamp,
clamp.Tensor and bitwise_and_/or_/xor_.Tensor.

CANN's aclnnForeach* kernels only process the FIRST 50 entries of an
aclTensorList. The ScalarList variants at least error past that (561002/161002);
Mul/Add/Addcmul/Lerp/Sqrt instead return success and leave entries >= 50
UNTOUCHED, so the bug is silent -- _foreach_lerp_ over 200 tensors "succeeded"
with 150 of them never written. The cap is on the entry count alone: measured
identical for numel 8..65536 and fp16/fp32/bf16. AdamW passes 310 tensors for
Qwen3-0.6B, so each foreach template is split into a <Kernel>Chunk doing the
aclnn call and a wrapper that slices the lists into sub-50 chunks. TensorList
and ArrayRef<Scalar> are both ArrayRefs, so the slicing is free, and elementwise
semantics make it exact.

Note for anyone probing other aclnn list limits: an exception-based search
reports "no limit up to 4096" for the silent ops. The values have to be compared
against CPU entry by entry.

aclnnForeachAddcdivScalarList's scalars param is a device aclTensor whose dtype
must match self (float32 scalars against fp16 inputs return 161002), so fp16
addcdiv lands a few ulp off CPU, which keeps the divisor at full precision.
AdamW holds optimizer state in fp32, so this does not reach the training path.

tests/integration/ops/test_foreach_dispatch.py covers all 7 ops at lengths that
straddle the boundary (51/60/128/310) and asserts on every entry, plus
AdamW(foreach=True) against foreach=False. Verified it fails (85 cases) when the
chunk size is raised to 128. e2e_qwen3_train_ascend.py no longer hard-pins
foreach=False; it takes --foreach/--no-foreach, applied to both backends.

Measured on 910 (Qwen3-0.6B, seq 128, batch 1, eager, card 10):
training 621.0 tok/s vs torch_npu 827.1 (0.75x, up from 0.69x at foreach=False's
507.9), loss curve unchanged at 2.937 -> 0.324. Inference unaffected at
24.5 tok/s. 169 foreach cases pass; ops/ suite and the qwen3 infer/train
integration tests show no regressions.

* feat(ascend): fused matmul in training via AutogradPrivateUse1 codegen

aten::matmul is CompositeImplicitAutograd, so claiming a fused aclnnMatmul
kernel on PrivateUse1 stops the mm/bmm/view decomposition -- and with it the
sub-op graph autograd was relying on. The op then binds its real derivative,
aten::matmul_backward, which the backend must supply. Until now that was
sidestepped by taking the fused path only when !requires_grad: inference got
one aclnnMatmul, training kept the decomposition.

Close the gap the way torch_npu does, by generating the missing autograd layer
rather than hand-rolling it. scripts/codegen_autograd.py drives torchgen's own
emit_body() -- the generator behind PyTorch's in-tree VariableType_N.cpp -- to
produce a VariableType::matmul on AutogradPrivateUse1 that builds
MatmulBackward0 and redispatches to the fused kernel. Only that thin layer is
generated: the backward node classes already ship in libtorch, so unlike
torch_npu we do not regenerate Functions.h/ADInplaceOrView/python bindings.
Adding an op is one entry in AUTOGRAD_OPS.

matmul_backward itself is implemented with two cached aclnnMatmul calls.
op-plugin's MatmulBackwardKernelNpuOpApi.cpp was the starting reference and has
two real bugs, deliberately not reproduced:

  * 2-D x N-D reshapes grad to {M, -1}. mat2^T flattens to (B*N, K), so its row
    index is the pair (b, n); grad must carry the same pair as its column
    index, which needs M permuted to the front first. The plain reshape pairs
    (m, n) against (b, n) and silently mixes batches.
  * Only *leading* singleton batch dims are stripped, so any interior or
    trailing broadcast returns a wrong-shaped gradient -- (2,1,3,4) x (2,5,4,6)
    yields a (2,5,3,4) grad for a (2,1,3,4) input. Replaced with at::sum_to
    onto the promoted operand shape, a no-op when nothing was broadcast.

Guarded by USE_ASCEND throughout: other backends have no fused kernel, keep
PyTorch's decomposition, and never bind a matmul_backward they cannot service.

Qwen3-0.6B training, batch 1 x seq 128, on a real 910:

  backward dispatches   5466 -> 4144   (torch_npu 4002)
  matmul_backward        n/a -> 253    (torch_npu 253)
  backward self-CPU    117.1 -> 109.9 ms/step
  throughput             566 -> 674 tok/s, 0.69x -> 0.82x torch_npu

Verified against a float64 CPU reference -- the kernel runs hf32 cube math, so
absolute fp32 comparison misreads ~1e-4 precision as a correctness bug. The new
test carries 30 cases and every shape in it catches a real defect in the
op-plugin rules on one side or the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard Ascend-only symbols so non-Ascend builds link and load

CUDA CI failed at `import torch_fl` with

  ImportError: libtorch_fl.so: undefined symbol:
    _ZN2at6native6flagos18MatmulKernelAscendERKNS_6TensorES4_

Two separate instances of the same mistake, both introduced by this branch.

1. register.cc referenced MatmulKernelAscend behind a *runtime* check
   (GetBackendForOp("matmul") == kAscend) with only the forward declaration
   guarded asymmetrically -- the backward decl was inside #if defined(USE_ASCEND),
   the forward one was not. A runtime branch does not remove a link-time
   reference, and backends/ascend/matmul.cc is not compiled without USE_ASCEND.
   A shared library links fine with undefined symbols and only fails at dlopen,
   which is why "Build wheel (CUDA)" passed and the failure surfaced at import.
   Fix: guard both the declarations and the call site at compile time.

2. copy_ops.cc / contiguous_ops.cc call ascend::StridedCopy and ascend::DtypeCast
   from #else branches that cover TsingMicro, GCU and MUSA-without-mudnn as well
   as Ascend, but included ascend_copy.h only under #ifdef USE_ASCEND. Those
   platforms failed to *compile* ("'ascend' has not been declared"); no CI builds
   them, so it stayed hidden. Fix: ascend_copy.h now supplies inline no-op
   fallbacks for non-Ascend builds (defined, not just declared, so nothing is
   left undefined at load), and is included unconditionally. The no-ops report
   "unavailable" and callers take the CPU round-trip they already implement.

Verified: register.cc compiled without USE_ASCEND has no undefined
MatmulKernelAscend reference (nm -u); copy_ops.cc and contiguous_ops.cc compile
clean under each of USE_TSINGMICRO / USE_GCU / USE_MUSA and with no macro at
all. On real 910: 30/30 matmul-backward, 175/175 foreach + conf-consistency,
3/3 Qwen3 training.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ai#56)

The C++ FlagGems path calls flag_gems' C++ entry points in liboperators.so,
JIT-compiling and launching Triton kernels without touching Python or the GIL.
It was CUDA-only on the torch_fl side because CMakeLists forced
FLAGGEMS_KERNEL=OFF for ACCELERATOR=metax. FlagGems itself already supports
MetaX (cpp/ -DFLAGGEMS_BACKEND=MACA), and its kernels reach the device through
the same DeviceBoxingGuard the metax boxing path uses, so no kernel code needed
changing -- only the build gate and the routing table.

- CMakeLists.txt: metax no longer force-disables FLAGGEMS_KERNEL. It stays OFF
  by default (a plain metax build needs no FlagGems) and turns on only in
  boxing mode when the caller explicitly asks, via
  FLAGGEMS_KERNEL=1 FLAGGEMS_DIR=<MACA FlagGems build>.
- csrc/CMakeLists.txt: fix an RPATH bug. The metax branch set INSTALL_RPATH
  wholesale, discarding the liboperators.so dir the top level appended to
  CMAKE_INSTALL_RPATH, so ldd reported "liboperators.so => not found". Re-add
  it when FLAGGEMS_KERNEL is on.
- setup.py: document the opt-in; the default -DFLAGGEMS_KERNEL=OFF is unchanged
  and the generic env pass-through emits the overriding -D.
- torch_fl/__init__.py: CPP + boxing selects backends_metax_flaggems_cpp.conf.
- new backends_metax_flaggems_cpp.conf: 17 flagos (C++) + 362 flagos_python
  + 1654 cuda. C++ ops keep flagos; non-C++ ops inherit the metax Python
  routing, so the existing metax fallbacks (bmm-via-SPLIT_K, mean.dim,
  slice_backward) still apply.

mm and mm.out route to cuda boxing (mcblas) instead of the C++ kernel: this is
a hardware limit, not an integration gap. flag_gems' mm_kernel_general requests
98304 bytes of shared memory and C550 provides 65536, so
mcModuleLaunchKernel returns mcErrorInvalidValue. The other 17 C++ ops pass.

Verified on real hardware (C550, metax 3.8.1, torch 2.10.0):
tests/manual/metax/test_flaggems_cpp_metax.py -- 24/24 ALL PASS. Each op is
checked for both routing (FLAGOS_LOG_DISPATCH shows "-> flagos") and numerics
against CPU; routing alone does not prove the kernel ran, since the log is
emitted before launch. Default path (CPP off) still selects backends_cuda.conf.
… layout fixes (flagos-ai#51)

* perf(copy): fast-path _to_copy via direct CUDA redispatch on CUDA/MetaX

_to_copy is one of the hottest ops in Qwen decode (FP32<->FP16 casts in
RMSNorm and attention). The flagos(PrivateUse1)->CUDA branch allocated an
intermediate contiguous tensor, memcpy'd device-to-device, then ran a
separate .to(dtype) cast -- an extra allocation and an extra pass.

Since flagos and CUDA share the same GPU memory, box self to CUDA and
redispatch straight to the native CUDA _to_copy with an explicit CUDA
DispatchKeySet (at::_ops::_to_copy::redispatch). That skips re-entering
the dispatcher from the top (no chance of routing back to PrivateUse1)
and lets the native kernel read self's strides + cast dtype in one
on-device pass, allocating the result directly on CUDA. self is unboxed
back to PrivateUse1 on DeviceBoxingGuard teardown, matching the boxing
pattern used by the generated CUDA kernels (e.g. PrivToCopyOutKernelCuda).

Guarded so only platforms with a CUDA runtime take the fast path;
USE_ASCEND/TSINGMICRO/GCU/MUSA keep the original contiguous + Memcpy path.

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

* perf(boxing): track boxed tensors by raw TensorImpl pointer

DeviceBoxingGuard and TensorListBoxingGuard recorded each boxed tensor in
a std::vector<at::Tensor>, so every box/unbox paid an intrusive refcount
atomic (fetch_add on push, fetch_sub on teardown) per tensor. Qwen decode
boxes/unboxes tens of tensors per token, so this stacks up on the hot
path.

Record raw c10::TensorImpl* instead and unbox by calling the new
SetTensorImplDevice directly (extracted from SetTensorDevice), skipping
the owning-Tensor round trip entirely -- zero refcount atomics on box or
unbox.

- SmallVector<TensorImpl*, 4>: the common case (<=4 tensor operands)
  stays on the stack, no heap allocation.
- The guard now takes a forwarding pack with a static_assert that every
  argument is an lvalue reference, rejecting temporary (rvalue) tensor
  handles at compile time so a recorded impl can never dangle.
- CPU-scalar / genuine-CUDA / undefined-tensor skipping and
  exception-safe restore semantics are unchanged; unboxing still routes
  through PyTorch's _change_backend_component_keys rather than poking
  dispatch-key bits directly.

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

* fix(flagos): register unfold view op to fix repeat data corruption

Tensor.repeat() calls aten::unfold internally to build a strided view
before copy_-ing into it. flagos had no PrivateUse1 impl for the plain
unfold view op (only unfold_backward / unfold_copy.out existed). Because
view ops cannot fall back to CPU -- storage can't be shared across
devices -- PyTorch emitted a warning and returned an uninitialized shell
tensor. repeat() then read garbage.

GPTJ's rotary embedding uses repeat() to build gather indices; corrupted
indices led to out-of-bounds gather -> illegal memory access -> crash.

unfold is pure stride computation (no GPU kernel): delegate to
at::native::unfold, which computes the new size/stride and calls
as_strided (already registered for flagos). Registered via direct m.impl
in the always-on block so it applies to every backend.

Verified on MetaX (t210-box): unfold/repeat now numerically correct, the
view-fallback warning is gone, and a tiny GPTJ forward runs cleanly
(cosine 1.0 vs CPU, rel err 0.05% -- fp32 drift, not corruption).

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

* fix(flagos): normalize group norm input before boxing

Diffusers UNet2DModel feeds GroupNorm channels-last (NHWC) or strided
(strides>1) inputs. The backend native_group_norm CUDA kernel requires
standard-contiguous layout (is_contiguous() == true); the native CUDA
path normalizes the layout before the kernel, but the flagos boxing
wrapper passed `input` straight through, raising:

  Expected X.is_contiguous(memory_format) to be true

Add a generic _CONTIGUOUS_TENSOR_ARGS_BY_OP map in codegen naming, per
op, which Tensor args must be forced contiguous before boxing. The
generated kernel now emits:

  at::Tensor input_contiguous = input.is_contiguous() ? input : input.contiguous();
  DeviceBoxingGuard guard(input_contiguous, weight_t, bias_t);
  auto result = at::native_group_norm(input_contiguous, weight, bias, ...);

Design points:
- The contiguous() must run before DeviceBoxingGuard: boxing rewrites the
  tensor's device metadata, so the copy has to happen while it still
  lives on flagos.
- is_contiguous() short-circuits to a no-op when already contiguous, so
  the only cost is on the exact non-contiguous inputs that would crash.
- Wired into gen_functional_pure / gen_out_variant / gen_tuple_return but
  currently applied only to native_group_norm.input -- precise, scoped.

Verified on MetaX (t210-box): channels_last / strided GroupNorm forward +
backward now numerically match CPU (max diff ~2e-7), a Diffusers-style
channels_last Conv->GroupNorm->SiLU ResnetBlock runs cleanly (cosine
0.99999988), and sort/layernorm/topk (same generators) are unaffected.
codegen unit checks pass, including the contiguous-before-guard ordering.

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

---------

Co-authored-by: zhangliheng <zhangliheng@chitu.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e, kernel metadata (flagos-ai#55)

* docs: PrivateUse1 profiler support design (Stage A ProfilerStubs + Stage B CUPTI child profiler)

* docs: PrivateUse1 profiler implementation plan (Stage A + Stage B, 8 tasks)

* docs: fix backend config path (torch_fl/configs/backends_cuda.conf)

* feat(profiler): guard.h carries real CUDA stream for event attribution

* feat(profiler): Stage A FlagosProfilerStubs implementation (blocked by torch 2.11 CPU wheel)

Implements torch::profiler::impl::ProfilerStubs subclass for flagos backend:
- record(): creates CUDA event with timing flag, records on current stream
- elapsed(): synchronizes events and returns elapsed time in microseconds
- onEachDevice(): iterates all flagos devices
- synchronize(): calls flagos DeviceSynchronize
- Static initializer registers stubs via registerPrivateUse1Methods()

Test added: test_stage_a_privateuse1_device_time checks for non-zero device time.

BLOCKER: torch 2.11.0+cpu wheel's Kineto library does not invoke the
PRIVATEUSE1_FALLBACK path at runtime. Stubs register successfully (enabled=true)
but record()/elapsed() are never called during profiling. Device times remain 0.

Evidence:
- ProfilerState::KINETO_PRIVATEUSE1_FALLBACK enum exists
- registerPrivateUse1Methods() succeeds, privateuse1Stubs() returns valid ptr
- Instrumentation confirms record() never invoked during profile() context
- CPU wheel likely omits PrivateUse1 fallback implementation (compile-time gate)

Next: try torch CUDA wheel, file upstream issue, or pivot to Stage B (NVTX).
See task-2-report.md for full diagnosis and unblock options.

* feat(profiler): Stage B CUPTI dlopen shim + cmake include path

- csrc/profiler/cupti_shim.h: header-only dlopen wrapper for CUPTI Activity API
  * Forward-declares CUPTI types (CUptiResult, CUpti_ActivityKind, callbacks) to avoid including cupti headers
  * Loads 8 Activity API functions at runtime (Enable/Disable/RegisterCallbacks/FlushAll/GetNextRecord/GetNumDroppedRecords/Push/Pop)
  * Tries libcupti.so in priority order: system CUDA 13.0 → .so.13 → .so.12 → generic
  * Exposes CuptiShim::get().available() + function pointers for Task 5 consumer
- csrc/CMakeLists.txt: add CUPTI include path discovery (find_path cupti_activity.h) + FLAGOS_HAVE_CUPTI macro
- tests/unit/test_profiler_privateuse1.py: add test_cupti_library_locatable() to verify runtime CUPTI availability

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(profiler): add CUPTI Activity API profiler (Stage B Task 5)

Implement libkineto::IActivityProfiler subclass using CUPTI Activity API
to collect GPU kernel traces. Register with kineto so torch.profiler
automatically captures CUDA kernel timelines for flagos backend.

Classes:
- FlagosCuptiProfilerSession: manages CUPTI lifecycle (enable/disable/flush)
- FlagosCuptiProfiler: factory registered with libkineto::api()

CUPTI buffer callbacks parse kernel/memcpy activity records and convert
to libkineto::GenericTraceActivity format.

LIMITATION: CUPTI Activity API requires initialization before CUDA driver
is loaded. In CPU wheel + dlopen'd libtorch_cuda.so architecture, CUDA
initializes before our module loads, so CUPTI silently no-ops (callbacks
never invoked). Implementation is correct for standard CUDA builds but
cannot capture activities in current environment. GPU profiling still
works via existing FlagosProfilerStubs (event-based timing) and NVTX
(external tools).

Files:
- csrc/profiler/flagos_cupti_profiler.h (98 lines): class declarations
- csrc/profiler/flagos_cupti_profiler.cc (307 lines): implementation
- tests/unit/test_profiler_privateuse1.py: updated test

Task: torch-fl profiler support - Stage B Task 5

* feat(profiler): Stage B correlation bridge - wire CUPTI push/pop for CPU→GPU linking

Implements Task 6: correlation ID bridge for linking CPU ops to GPU kernels.

Changes:
- Add instrumentation counters (g_correlation_push_count/pop_count) to track
  push/pop invocations in FlagosCuptiProfilerSession methods
- Export C API (flagos_cupti_get_correlation_push_count, _pop_count,
  _reset_correlation_counters) with visibility("default") for Python testing
- Add test_stage_b_correlation_or_degrade that verifies push/pop are called
  during profiling (3/3 times for single matmul op)

KNOWN LIMITATION (degraded acceptance):
In the CPU-wheel environment, CUPTI buffer callbacks are never invoked
(CUDA initializes before CUPTI registers), so GPU activities are not captured
and correlation IDs don't actually link anything. This commit proves CODE
CORRECTNESS (push/pop methods call CUPTI functions correctly) rather than
functional correlation (which is impossible without a torch+cuda wheel or
custom torch build with working CUPTI Activity API).

Test result: PASS (degraded mode - push/pop called, 0 GPU activities captured)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: Task 7 validation + add DeviceGuardImpl::getDeviceCapability

Validation results:
- qwen3_infer: 4/4 PASS
- ops regression: 323 passed, 20 failed (matches Task 1 baseline exactly)
- qwen3_train: 3 errors (pre-existing autograd issue, not a regression)

Also includes guard.h change from Task 6 that wasn't committed:
- Add getDeviceCapability() override to FlagosGuardImpl
- Returns default DeviceCapability struct (all scalar types enabled)
- Required by torch 2.11's autograd engine

Known limitation: qwen3_train fails with 'Backend doesn't support getting
device capabilities' error. This is a pre-existing issue documented in
Task 1 baseline (20 ops tests fail with same error). The getDeviceCapability
implementation is correct but not being invoked by autograd, likely due to
vtable/initialization timing issues in the CPU-wheel + external-CUDA-lib
architecture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(profiler): make CUPTI actually capture the GPU kernel timeline

Stage B previously registered a CUPTI child profiler that captured 0
activities. Root-caused (see standalone probes) to three real bugs, all
fixable within the CPU-torch + external libtorch_cuda.so architecture
(no CUDA wheel needed):

1. Version mismatch: CuptiShim dlopen'd the system CUDA-13.0 libcupti
   first, but the process runs the cu12.8 runtime (libtorch_cuda ->
   NEEDED libcudart.so.12; _preload_cuda_assets loads the pip cu12
   libcupti.so.12). Arming the wrong libcupti no-ops. Now bind the
   already-loaded copy via dlopen(NULL)+dlsym, then prefer libcupti.so.12.

2. Wrong enum value: CUPTI_ACTIVITY_KIND_CONCURRENT_KERNEL is 10 in cu12,
   not 9. The wrong value enabled/matched the wrong activity kind.

3. Wrong record layout + self-deadlock:
   - The activity-record structs were hand-guessed (natural alignment,
     name at a fixed offset) and decoded garbage (empty names, dur=0).
     Replaced with byte-accurate __packed__ mirrors of the cu12
     CUpti_ActivityKernel9 / CUpti_ActivityMemcpy layouts; kernel name is
     a const char* field. CMake now prefers the version-matched cu12
     cupti_activity.h.
   - start() held g_session_mutex across cuptiActivityFlushAll(), which
     synchronously re-enters bufferCompleted (same non-recursive mutex) ->
     hang in torch.profiler start_trace. Scope the lock to publishing the
     session only; flush after release.

Also arm CUPTI (RegisterCallbacks + ActivityEnable) at module-load time
(before the first CUDA context) instead of only in session start(), and
gate all diagnostic logging behind FLAGOS_CUPTI_SHIM_DEBUG.

Result: torch.profiler now captures the real CUDA kernel timeline with
correct names and microsecond durations. Unit test asserts ~N sgemm
kernels for N matmuls; qwen3-0.6B inference captures 2261 named GPU
kernels. Ops regression unchanged (323 passed / 20 pre-existing fails).

* profiler: decouple CUPTI binding from any one CUDA version

The shim previously reached for a hardcoded /usr/local/cuda-13.0 path and a
cu12-biased soname ordering, and the record decoder trusted a hand-mirrored
CUpti_ActivityKernel9 layout unconditionally. Both tie the profiler to one
CUPTI release, and the second fails silently: a layout mismatch yields a trace
of empty names and zero durations rather than an error.

Binding: an explicit FLAGOS_CUPTI_LIBRARY wins first, then whatever libcupti is
already loaded in the process (by construction the copy the running CUDA
runtime pulled in), then a soname fallback ordering. The override has to
outrank the already-loaded copy -- the case that motivates setting it is a
preloaded CUPTI we cannot decode, so an override checked only when nothing was
loaded would be dead in the one case it is advertised for.

Decoding: validate before trusting the mirror. end >= start, non-zero start, a
duration under an hour, and a readable non-empty name are all true of any sane
kernel record on any CUPTI version. Records failing these are dropped, and one
diagnostic naming the bound library and API version replaces the mystery empty
timeline.

Verified on A100 under torch-fl-211: 21/21 kernels with real names and non-zero
durations against both the pip cu12 CUPTI (API 26) and the system CUDA-13 one
(API 130000) via the override, confirming the checks do not false-positive
across versions; and with the threshold temporarily forced to 1ns, every record
is rejected with exactly one diagnostic and no crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: design for torch-cuda profiler parity

Stage B captures the GPU kernel timeline correctly, but a measured diff against
a real torch+cu128 env shows what is still missing. Same code, both stacks:
flow arrows 59 vs 0, cuda_runtime events 34 vs 0, kernel arg fields 13 vs 0,
kernel names demangled vs mangled, and device time landing on aten::mm vs only
on kernel names.

Five of those six rows share one root cause: we never collect RUNTIME activities.
cuda_runtime is the hub that holds both the External id linking back to cpu_op
and the correlation linking forward to the kernel, so without it the whole chain
is severed. Two mechanisms found while measuring: processTrace has a four-arg
overload whose getLinkedActivity callback is the official channel for filling
linked/flow (we only override the one-arg version, hence zero flows), and the
Stage A ProfilerStubs sits on an unreachable else-branch -- its recorded skip
reason was wrong, though the conclusion that the path is dead was right.

Design keeps the external IActivityProfiler and splits it into a vendor-neutral
DeviceTracer interface plus a CUPTI implementation, so adding ascend later means
one new tracer and no changes to the kineto layer. Acceptance is a structural
diff against a baseline snapshot committed to the repo, which keeps CI free of
any CUDA torch dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: add profiler CUDA parity implementation plan

7 tasks: verification gate, DeviceTracer interface, CUPTI extraction,
kineto adaptor rewrite, 13-field metadata, baseline snapshot + CI, cleanup.

Refs #spec docs/superpowers/specs/2026-08-03-profiler-cuda-parity-design.md

* feat(profiler): verify correlation foundation — RUNTIME activity + flow/device-time metrics

Proves 3 design claims before refactoring:
1. Flow arrows (ac2g) appear in chrome trace — 28 entries = 14 paired s->f
2. key_averages aten::mm gains self_device_time_total — ~816us, exactly the
   sum of the 5 ampere_sgemm kernels in the same trace
3. Runtime events materialize with category privateuse1_runtime — 258 events

ActivityType: PRIVATEUSE1_RUNTIME (CUDA_RUNTIME fallback never needed)
Metrics: 28 flows (14 paired), 815.97us device time on aten::mm

Two spec corrections measured by ablation, see task-1-report.md:
- "device time attribution is free" is FALSE: suppressing activity.linked
  with everything else identical drops aten::mm from ~816us to 0us. Free
  only in the sense that no Python-side change is needed.
- EXTERNAL_CORRELATION (kind 39) is a required step the plan omits:
  getLinkedActivity() keys on torch correlation ids, CUPTI records carry
  CUPTI ids, and only these records bridge the two.

Also: flow arrows need BOTH halves with a shared CUPTI correlation id, gated
on the correlation having a device-side activity. Keying on the torch id
emitted 203 unpaired 'f' halves that still passed a bare count>0 assertion.

Refs #spec 2026-08-03-profiler-cuda-parity-design.md §3.2

* fix(profiler): review round 1 — pairing assertion, window filter, deferred RUNTIME arming

Addresses 6 Important review findings on the Task 1 verification gate.

1. metric 1 asserted a bare ac2g count, reproducing the false-pass documented
   in the task report. verify_flow_arrows now counts *paired* s<->f ids and
   requires count(s)==count(f) and equal id sets. 28 entries -> 12 renderable
   arrows. The old 203-unpaired-'f' trace would now FAIL.
2. metric 2's ground-truth cross-check was a manual observation. Now automated:
   sums dur over sgemm kernel events and requires |attributed - sum| within
   max(1us, 1%). Measured delta 0.000us.
3. EXTERNAL_CORRELATION parse branch had no validation — the one branch whose
   silent misdecode is invisible (garbage externalIds -> nullptr links ->
   device time silently back to 0). Now checks externalKind == CUSTOM0 via
   reportLayoutMismatch, consistent with the kernel/memcpy branches.
4. lookupExternalCorrelation returned 0 for both "not found" and a real torch
   correlation id (id 0 is a live value: autograd/profiler.py treats it as a
   frontend event). Now returns std::optional; unmapped activities never link.
5. startTime/endTime were accepted and dropped, shipping out-of-window events
   (66/258 runtime events before the first cpu_op; a 250ms "cudaLaunchKernel"
   against a 157ms span). Now filtered. Units verified empirically, not
   assumed: cuptiGetTimestamp is 2.7us from CLOCK_REALTIME and kineto's window
   is the same epoch-ns domain, so they compare directly — a MONOTONIC reading
   would have been off ~1.78e18ns and filtered everything. Filter applied both
   when building device_correlations and at emit, so a dropped kernel cannot
   leave a dangling flow half. Result: 0 events outside the window.
6. RUNTIME+EXTERNAL_CORRELATION armed at import cost every non-profiling user.
   MEASURED with a launch-bound workload (a GPU-bound one is insensitive and
   would have wrongly cleared it): +22%, 10.56 -> 12.88us/op over 3 A/B pairs,
   with ~15x more variance, far outside the ~0.1ms spread. Moved both kinds to
   session start(); ActivityRegisterCallbacks still runs at import, which is
   what cupti-must-arm-before-cuda-context actually requires. Overhead gone
   (10.48us/op) and all metrics still pass.

Re-measured: 12 paired flow arrows, 815.13us aten::mm device time (cross-check
delta 0.000us), 192 privateuse1_runtime events, window containment PASS.
tests/unit/test_profiler_privateuse1.py: 4 passed, 1 skipped.

* feat(profiler): add vendor-agnostic DeviceTracer interface

Pure abstract class + DeviceEvent struct with open metadata map.
DeviceEvent carries both correlation_id (CUPTI) and external_correlation_id
(torch) to support kineto's getLinkedActivity() callback in Task 4.
Stub factory in flagos_cupti_profiler.cc (moved to cupti_device_tracer.cc in Task 3).

Refs #spec 2026-08-03 §2.1

* refactor(profiler): extract CUPTI logic to cupti_device_tracer.cc

CuptiDeviceTracer implements the DeviceTracer interface: it owns every CUPTI
detail (buffer callbacks, activity-record layout mirrors, the layout
self-check, external-correlation resolution) and hands the layer above a flat
vector of vendor-neutral DeviceEvents. flagos_cupti_profiler.{h,cc} is now a
pure kineto adaptor with zero CUPTI types or calls -- Task 4 only has to rename
it.

Behaviour changes beyond the move:

* MEMSET is now collected (design §4.1) and mapped to GPU_MEMSET/gpu_memset.
  cuBLAS zeroes a 512B workspace per gemm, so aten::mm legitimately gains ~9us
  of memset device time on top of its sgemm kernels. The Task 1 gate's ground
  truth was sgemm-only and had to learn about it; it now sums every device
  event linked to aten::mm (stricter -- it also catches a memset attributed to
  the wrong op) plus a separate assertion that kernels remain >90% of the
  total, so losing kernel collection cannot pass by shrinking both sides.
  Confirmed by same-binary A/B: disabling only the MEMSET ActivityEnable
  returns attribution to the sgemm sum exactly (delta 0.000us).

* Runtime events are named from their cbid instead of being hardcoded to
  "cudaLaunchKernel", which previously reported blocking synchronizes and every
  malloc/free as kernel launches. The 22-entry table is cross-checked three
  ways -- pip cu12 header, system CUDA-13.0 header, and a live
  cuptiGetCallbackName(CUPTI_CB_DOMAIN_RUNTIME_API, cbid) query -- all agreeing.
  `_ptsz` variants map to their base name so a per-thread-default-stream build
  does not fall through to the generic label.

* deviceCount() resolves cudaGetDeviceCount via RTLD_DEFAULT (returns a real 8
  here) rather than hardcoding a guess; libcudart is in the process via the
  external libtorch_cuda.so but not on our link line.

* The adaptor builds fmt header-only: kineto's addMetadata template calls
  fmt::format and libtorch_cpu.so exports no fmt::vformat, so emitting kernel
  metadata otherwise fails to link at import time.

External-correlation resolution moved with the rest and now runs in drain()
rather than per-record, because CUPTI does not order an activity record against
the EXTERNAL_CORRELATION record that maps it. Left std::nullopt on a miss, never
0 -- 0 is a valid torch correlation id, and collapsing the two silently
mis-attributes device time.

Verified: correlation gate ALL METRICS PASSED (18 paired flow arrows, aten::mm
824.125us vs device-event sum 824.125us delta 0.000us); unit tests 4 passed,
1 skipped.

Refs #spec 2026-08-03 §2

* fix(profiler): diagnose empty getLinkedActivity, guard buffer alloc, pin cbid mapping

Review round 1 follow-ups for Task 3.

1. An empty getLinkedActivity callback silently produced linked == nullptr for
   every activity -- the exact ablation state Task 1 measured returning
   self_device_time_total to 0 -- with no log at any verbosity. Warn once,
   ungated (same rationale as reportLayoutMismatch: rare by construction,
   catastrophic when it happens). The summary log now reports linked/candidates
   plus resolver=present|EMPTY, so "0 linked because nothing ran", "0 linked
   because every id was rejected" and "0 linked because there is no resolver"
   are three distinguishable states rather than one ambiguous "0/N".
   Verified by temporary A/B forcing the empty branch: diagnostic fires and
   aten::mm device time drops to 0, confirming the warning marks the real
   failure mode; log reads "linked 0/209 ... resolver=EMPTY" vs "209/209 ...
   resolver=present" normally.

2. bufferRequested ignored aligned_alloc's return value, handing CUPTI a null
   pointer while claiming size = 8MB. Pre-existing code moved verbatim in the
   previous commit, but this file now owns buffer management. On null, decline
   the buffer properly (*size = 0, *maxNumRecords = 0) and warn once, degrading
   to dropped records instead of a write through address 0 under memory
   pressure.

3. Interim guard for the cbid->name table until Task 6's integration test lands:
   the gate now asserts at least one runtime event carries a name that is
   neither "cudaRuntime" (the default: fallback) nor "cudaLaunchKernel". That is
   precisely the invariant the old hardcode violated. Negative-tested by
   rewriting every runtime name to "cudaLaunchKernel" in a captured trace: the
   check returns False and fails the script.

Gate: ALL METRICS PASSED (18 paired arrows; aten::mm delta 0.000us across 3
consecutive runs; new metric3b PASS). Unit tests: 4 passed, 1 skipped.

* refactor(profiler): rename flagos_cupti_profiler → flagos_kineto_profiler

The profiler adaptor is now vendor-agnostic (CUPTI logic moved to
cupti_device_tracer.cc in Task 3). This rename reflects that role.

Changes:
- File rename: flagos_cupti_profiler.{h,cc} → flagos_kineto_profiler.{h,cc}
- Class rename: FlagosCuptiProfiler{,Session} → FlagosKinetoProfiler{,Session}
- Function rename: registerFlagosCuptiProfiler → registerFlagosKinetoProfiler
- C API rename: flagos_cupti_*_correlation_* → flagos_kineto_*_correlation_*
  (exported symbols; test updated to match)
- Profiler name() string: "flagos_cupti" → "flagos" (no consumers found)
- Local debug helpers: flagos_cupti_debug/FLAGOS_CUPTI_LOG → flagos_kineto_*
  (adaptor copy only; cupti_device_tracer.cc retains its own)

Verification:
- verify_correlation_foundation.py: all 5 metrics PASS
  (18 flow arrows, aten::mm device time 824.123µs, cross-check delta 0.000µs)
- test_profiler_privateuse1.py: 4 passed, 1 skipped (correlation test now passes)

* fix(profiler): quote non-numeric metadata values in kineto trace JSON

kineto's GenericTraceActivity::addMetadata stores every value with
quoted=false, and metadataJson() emits it as `"key": <raw>`. That is safe
only while every value happens to be a JSON literal -- which is true today
purely by accident (all current values are numbers or bracketed lists).

The blast radius is the reason to fix this BEFORE adding fields: kineto
concatenates all activities into one document, so a single bare identifier
("N/A", a memory-kind label, a name) makes json.load() fail on the ENTIRE
trace file, not just the offending event.

DeviceEvent::metadata is map<string,string>, so values are already text by
the time the adaptor sees them and cannot be classified by static type.
metadataValueIsJsonLiteral() classifies by textual form instead, and is
deliberately sound only in the "may I skip quoting?" direction: a spuriously
quoted number is cosmetic, an unquoted non-literal is fatal. The quoted path
escapes " and \ because addMetadataQuoted does no escaping of its own.

No behaviour change today (every current value classifies as a literal);
this is the safety net for the metadata fields that follow.

* feat(profiler): add kernel/memcpy/memset metadata parity + occupancy

Brings every device event's `args` dict up to what torch-cuda emits, so the
two traces can be diffed structurally instead of eyeballed.

  kernel      5 -> 13 keys  (+queued, device, context, stream, correlation,
                             blocks per SM, warps per SM,
                             est. achieved occupancy %)
  memcpy/set  2 ->  7 keys  (+device, context, stream, correlation,
                             memory bandwidth (GB/s))
  runtime     1 ->  3 keys  (+cbid, correlation)

`External id` is NOT added here -- kineto's own logger emits it from
linked/correlationId, and adding ours would produce a duplicate JSON key.

Kernel9 mirror extended past `name` (reserved0/queued/submitted) to reach the
`queued` timestamp. Offsets cross-checked against the installed CUPTI header:
name=104, queued=120, submitted=128. The pip cu12 header (the copy actually
bound at runtime) and the system CUDA-13 header are byte-identical here, so
this is no more version-fragile than the prefix already mirrored.

OCCUPANCY. Derived from CUDA's header-only occupancy calculator (no linking, no
context); device attributes come via dlsym'd cudaDeviceGetAttribute, mirroring
how deviceCount() already resolves cudaGetDeviceCount, and are cached per device
because this runs in the buffer-processing hot path.

Two things measurement settled, both of which a plausible-looking formula gets
wrong:

  * Only occupancy clamps. "blocks per SM"/"warps per SM" are reported UNCAPPED
    -- a live 2048-block kernel reports 151.703705 warps/SM, far above the SM's
    64-warp capacity.
  * "warps per SM" uses PLAIN division by warpSize, not a ceiling. A 16-thread
    kernel reports 0.004630; a ceil() form gives 0.009259, i.e. double. The two
    agree for every block size that is a multiple of 32, which is why this only
    shows up on a kernel like that one.

Values AND their text formatting are reproduced: occupancy fields as fixed
6-decimal floats (float, not double -- 640/108*4 prints 23.703703 as a float
and 23.703704 as a double, and torch-cuda reports the former), bandwidth as
shortest-roundtrip. Verified against live torch-cuda traces on identical
workloads: 18/18 kernel configurations match exactly on all three derived
fields, including the bs=16 and 151.703705 cases above.

Two judgment calls, both toward "say nothing rather than something false":
  * queued: emit 0 when the field is not populated (matches torch-cuda, whose
    traces show 0). CUPTI was MEASURED writing a literal 0 here, not the
    documented CUPTI_TIMESTAMP_UNKNOWN, so both spellings are rejected --
    checking only the documented sentinel would pass a bogus 0 through as real.
  * device attributes unavailable: omit the three occupancy keys rather than
    emit zeros. A 0.0 occupancy is indistinguishable from a real measurement of
    a badly-occupied kernel; an absent key is honest and a parity test says so.

Also widens the metadata JSON-literal classifier to accept exponent notation:
%g emits "8e-05" for small bandwidths, which is valid JSON, and quoting it
would silently break parity on exactly the values torch-cuda emits bare.

Verified: Task 1 gate all metrics (18 paired flow arrows, aten::mm 825.5us,
cross-check delta 0.000us); trace json.load()s; arg-key sets identical to
torch-cuda for all four categories on two workloads; 4 passed 1 skipped.

* test(profiler): add CI parity test + torch-cuda baseline snapshot

Baseline: 13-key kernel args, 7-key memcpy/memset, 3-key runtime, ac2g flows.
Test asserts: category coverage, PAIRED flow arrows, arg-key supersets,
device-time attribution with cross-check, demangled kernel names, and
non-hardcoded runtime names (the cbid-table regression guard).

CI entry added to .github/configs/cuda.yml integration_tests list.

Refs #spec 2026-08-03 §5

* feat(profiler): achieve torch-cuda parity — flows, metadata, device time attribution

Removes the scaffolding: unreachable flagos_profiler_stubs.cc and the two
tests/scratch verification scripts, whose coverage now lives in the CI parity test
(window containment ported in this commit).

Adds docs/profiler.md: three-layer architecture, the two correlation-id schemes,
the CUPTI arming constraint, debug env vars, and known gaps.

Refs #spec 2026-08-03 §6

* chore: untrack machine-local .libtorch_cuda_assets symlink

It was committed as an absolute symlink into /nfs/lvyufeng/, which resolves
nowhere on any other checkout. setup.py and scripts/with_cuda_libtorch.sh
already reference the path without requiring it to be tracked, so ignore it
instead.

* style(profiler): satisfy pinned ruff on the profiler test files

Consolidate scattered mid-file imports to the top, drop unused bindings, and
apply ruff format. CI runs lint first with every other job declaring
needs: lint, so these would have blocked the build and integration jobs.

* docs(profiler): write docs/profiler.md in English

Matches the language of the surrounding docs/ tree.

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProcessGroupFlagOS delegated to the inner backend without pinning the current
device. The inner backend resolves the device it works on from whatever is
current, not from the tensor, so a caller that binds only with
torch.cuda.set_device(rank) -- the natural thing to do -- had its collectives
enqueued on the wrong device.

The current device does not stay where the caller put it. Measured on DTK:

    torch.cuda.set_device(1)          -> cuda=1  flagos=0
    torch.ones(4, device='flagos:1')  -> cuda=0  flagos=0

Allocating a flagos tensor resets the *cuda* current device to flagos's. flagos
and cuda track separate currents even though flagos aliases the same physical
GPU, so after the first allocation every rank had device 0 current.

FlagCX turns that into a GPU fault rather than a wrong answer: getStreamByIndex
lazily streamCreate()s one stream on the current device and caches it by index
alone, so the first collective binds the comm to device 0 permanently. Rank 1
then enqueued device-1 buffers onto a device-0 stream and RCCL faulted
(VMFault / "invalid resource handle") on the first all_reduce. A process kill,
not an exception.

Guard in the wrapper rather than asking callers to set both devices: c10d hands
us the tensors, so this is the one layer that always knows the right device.
Same reasoning as the DeviceGuard added to the CallPythonOp_* callers in flagos-ai#54,
one level up. _comm_device_guard moves and restores both currents, and is
applied to all 18 tensor-carrying collectives.

barrier carries no tensor to infer a device from, so it replays the index an
earlier collective bound the comm to; FlagCX rejects a barrier issued from
anywhere else with "flagcx communicator was initialized with different device".

Also merges two pairs of duplicate _allgather_base / _reduce_scatter_base
definitions, where the second silently shadowed the first.

test_flagos_dist_live.py passed with this bug present -- it happened to leave
the two currents agreeing -- so the new test deliberately calls only
torch.cuda.set_device(rank). Validated by neutering the guard: the test
reproduces the VMFault (exit 1), and passes (exit 0) with the guard restored.

Measured on 8x Hygon DCU (DTK), FlagCX inner backend:

    2 cards  10/10 collectives OK  DDP grads identical  exit 0
    4 cards  20/20 collectives OK  DDP grads identical  exit 0
    8 cards  40/40 collectives OK  DDP grads identical  exit 0

matching the RCCL baseline. RCCL path re-checked after the change (4 cards,
20/20, exit 0): no regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@lvyufeng
lvyufeng force-pushed the fix-comm-device-index branch from 0a2c22c to a74a9cc Compare August 6, 2026 05:27
@lvyufeng

lvyufeng commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Opened by mistake against my own fork; the real PR is flagos-ai#57.

@lvyufeng lvyufeng closed this Aug 6, 2026
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.

3 participants