Status: design proposal. 2026-08-13. Motivated by Kunlun P800, which torch-fl currently does not support at all, but written so the harness is reusable for every subsequent target rather than being P800-specific.
An earlier draft of this proposal modelled new-target bringup as a single end-to-end command. That is the wrong decomposition, because the work splits along two independent axes that do not co-vary:
- Which torch version are we building against? This is hardware-independent.
It is about ATen schemas, dispatcher signatures, and which
torch==X+cpuwheel the build resolves against. - How does this chip execute an operator? This is hardware-specific and has
two fundamentally different answers — reuse a vendor-supplied CUDA-compatible
libtorch_cuda.so, or call the vendor's own operator library directly.
Collapsing these into one command means every torch bump re-runs chip discovery, and every new chip re-derives torch-version facts that are already known. It also hides the one piece of work that is unconditionally required: the device runtime.
The runtime is the honest common denominator. Whether a chip is CUDA-compatible
or not, torch-fl cannot allocate a tensor on it until the flagos runtime
contract is implemented. So the runtime gets its own command, and it is a
prerequisite for both operator paths.
┌───────────────────────────┐
│ /torch-version-port │ hardware-independent
│ (axis: torch version) │ runs on its own cadence
└───────────────────────────┘
┌───────────────────────────┐
│ /runtime-bringup │ ALWAYS required
│ (the ~40-function floor) │ chip-specific, op-agnostic
└─────────────┬─────────────┘
│ prerequisite
┌─────────────┴─────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ /cuda-compat-vendor │ │ /native-op-backend │
│ extract + bundle the │ │ bind the vendor op │
│ vendor libtorch_cuda │ │ library per operator │
└───────────────────────┘ └───────────────────────┘
CUDA-compatible not CUDA-compatible
(metax, dcu, ppu) (ascend, gcu, musa)
/runtime-bringup is not a subroutine of the other two — it is a gate. Its exit
criterion (tensors allocate, copy H2D/D2H, streams and events synchronize) is
checkable without a single operator being registered, and both operator commands
assume it has already passed.
The "basic 40 functions" is close to exact. Counted from the tree rather than from memory:
| Surface | File | Count |
|---|---|---|
| C ABI runtime functions | csrc/include/flagos.h (FLAGOS_EXPORT Error_t) |
28 |
| Caching-allocator virtuals | csrc/runtime/allocator/device_memory_interface.h (= 0;) |
10 |
| Vendor stream accessor | GetDefault<Vendor>Stream / GetCurrentStream |
1–2 |
| Build wiring | csrc/runtime/accelerator/CMakeLists.txt + setup.py selector |
2 sites |
So 38 functions plus wiring — call it 40. The 28 C functions land in exactly three files per vendor, and every existing backend implements all 28 (verified: cuda, musa, gcu, ascend, tsingmicro, bpu each expose 28):
csrc/runtime/accelerator/<vendor>/
├── device.cc # 5 GetDeviceCount, GetDevice, SetDevice,
│ # DeviceGetStreamPriorityRange, DeviceSynchronize
├── memory.cc # 9 Malloc, Free, MallocHost, FreeHost, Memcpy, MemcpyAsync,
│ # PointerGetAttributes, Memset, MemsetAsync
└── stream.cc # 14 Stream{CreateWithPriority,Create,GetPriority,Destroy,
# Query,Synchronize,WaitEvent}
# Event{CreateWithFlags,Create,Destroy,Record,
# Synchronize,Query,ElapsedTime}
Plus csrc/runtime/allocator/backends/<vendor>_memory.h implementing the 10
virtuals (device_malloc, device_free, get_device_index, set_device,
get_memory_info, event_create, event_destroy, event_record,
event_query, memcpy), with the optional provides_caching() escape hatch for
platforms that already ship a mature caching allocator.
This is a fixed, enumerable, mechanically checkable contract. That is what makes it a good harness target: the agent is not designing anything, it is filling in a known table from a vendor SDK header. Completeness is verifiable by counting symbols, not by judgement.
Axis: torch version. Hardware: none.
Adapt torch-fl to a different torch==X.Y+cpu line. Everything this command
touches is schema- and ABI-level; nothing in it knows what chip will eventually
run the kernels.
/torch-version-port 2.9- Create/checkout the version branch. Branches are per-torch-minor in this
repo (
maintracks 2.10.x;2.9is a sibling, not a descendant of a chip port). Establish the branch before any other command runs. - Pin the environment.
torch==2.9.x+cpu— CPU-only, deliberately. The CUDA-compatible path supplies GPU symbols out-of-band (command 3), so a CUDA pip torch in the env is a liability, not a help. - Re-run ATen codegen against the new schema.
scripts/codegen/codegen_ops.pyreads torchgen's packagednative_functions.yaml, so a torch bump changes its output. Regeneratecsrc/aten/generated/{ops.h,ops.cc,cuda_kernels.cc,register.inc}. - Reconcile per-operator signature splits. The known failure mode is
Mismatch in kernel C++ signaturesatimport torch_fl, caused by ops whose dispatcher signature usesIListRefvsArrayRefdifferently between versions. The existingcuda-op-integrationskill documents this; this command inherits that procedure and itsARRAYREF_OPSlist. - Prove codegen idempotency. A second run must produce no diff. This is a hard gate — a non-idempotent generator makes every future rebase a conflict.
- Update the declared range.
pyproject.toml,docs/reference/compatibility.md(thePyTorchrow currently reads>=2.10,<2.11), and the README badge.
python scripts/codegen/codegen_ops.pyemits the full conf op count with no WARNINGs- second codegen run leaves
git diffempty import torch_flclean, no signature mismatchtorch.__version__still ends in+cpu- existing platform tests for at least one already-supported chip still pass
This command is a generalization of cuda-op-integration, which today is
written specifically around CUDA boxing on a new torch branch. The version-port
concerns (codegen, signature splits, idempotency) belong here; the
CUDA-boxing-specific concerns (external libtorch_cuda.so, LD_PRELOAD) belong
in command 3. Splitting them is the main refactor this design implies for the
existing skill.
Axis: hardware. Operator-agnostic. Required for every target.
Implement the 38-function contract in §2 for a new chip and nothing more. No
operators, no boxing, no kernel library. The deliverable is a build in which a
flagos tensor can be allocated, filled from host memory, copied back, and
synchronized — with every operator still falling back to CPU.
/runtime-bringup kunlun --sdk-path /opt/kunlun/xreDo not use xpu as the FLAGOS_ACCELERATOR value for Kunlun. torch.xpu is PyTorch's
own Intel GPU namespace, and a FLAGOS_ACCELERATOR=xpu selector inside a PyTorch
plugin will be read as Intel by every future reader. Use kunlun.
Discover, from the vendor SDK, the concrete answer to each of the 38 slots.
Output is a structured mapping report, committed as
docs/vendors/<vendor>/runtime-api-map.md:
- the runtime shared library and its header set (for Kunlun: XRE —
libxpurt.soandxpu/runtime.h, to be confirmed against the actual SDK, not assumed) - device: enumerate / get / set / synchronize entry points
- memory: device alloc/free, pinned-host alloc/free, sync + async memcpy, memset, pointer attribute query, and free/total memory query
- stream & event: create-with-priority, query, wait, elapsed-time. Priority range is the slot most often absent; record the clamp behaviour rather than inventing one
- error enum →
Error_tmapping (Success,ErrorNotReady,ErrorInvalidDevice,ErrorMemoryAllocation,ErrorUnknown)
Two findings must be recorded explicitly because they change the generated code rather than just filling it in:
- Missing slots. If the SDK has no event-elapsed-time, or no async memset,
that is a real capability gap. It must be recorded and surfaced, not silently
stubbed with
return Success. A stub that lies about completion produces silent wrong results downstream. - Whether the vendor ships a caching allocator. Determines
provides_caching(). Getting this wrong costs either performance (redundant double-caching) or correctness of memory stats.
Emit device.cc / memory.cc / stream.cc and
allocator/backends/<vendor>_memory.h from the mapping report, using an existing
backend as the structural template. Pick the template by shape, not
alphabetically: musa for a CUDA-shaped runtime with renamed symbols, ascend
for a runtime with its own idioms and a self-managed allocator.
csrc/runtime/accelerator/CMakeLists.txt— anelseif(FLAGOS_ACCELERATOR STREQUAL "<vendor>")arm for the source subdirectory and one for include/link paths (both lists exist separately in that file; both need the arm)setup.py— theFLAGOS_ACCELERATORbranch for SDK detection, with a clear actionable error when the SDK is absent, matching the DTK/BPU precedenttorch_fl/configs/backends_<vendor>.conf— created, and for this command deliberately near-empty: every operator falls back to CPU
import torch, torch_fl
t = torch.empty(4, 4, device="flagos") # allocator reached
h = torch.arange(16, dtype=torch.float32).reshape(4, 4)
d = h.to("flagos"); back = d.cpu() # H2D + D2H round-trip
assert torch.equal(h, back)
torch_fl.synchronize() # stream/event path live
print(torch_fl.device_count(), torch_fl.memory_allocated())Symbol-count check as a mechanical gate:
grep -c 'FLAGOS_EXPORT Error_t' csrc/include/flagos.h # expect 28
grep -oE '^Error_t [A-Za-z]+\(' csrc/runtime/accelerator/<vendor>/*.cc | wc -lThe two numbers must agree, and every unimplemented slot must be an explicit documented gap rather than a missing symbol found at link time.
Axis: hardware. Precondition: command 2 passed.
For a chip whose vendor ships a CUDA-compatible PyTorch build, harvest the
vendor's own libtorch_cuda.so and reuse PyTorch's CUDA kernels through zero-copy
device-metadata boxing. Zero kernels are written.
/cuda-compat-vendor kunlun --vendor-torch /opt/kunlun/torchdocs/vendors/cuda/external-libtorch-cuda.md records the measured result: a
CPU-only pip torch plus an externally supplied libtorch_cuda.so registers real
CUDA implementations for aten::mm / add / _softmax / bmm and computes
correct results (mm max_err = 9.5e-06). The hard constraint is load timing:
the library must be loaded before import torch, hence LD_PRELOAD and
scripts/vendor/with_cuda_libtorch.sh.
This command must not be entered on a hunch. The check is concrete: take the vendor's torch install and confirm it registers a CUDA dispatch key rather than occupying PrivateUse1.
python -c "
import torch
print(torch.__version__)
print(torch._C._dispatch_dump('aten::mm'))
"- CUDA key present → this command applies (the metax / dcu / ppu pattern).
- PrivateUse1 → stop and use command 4. torch-fl itself occupies
PrivateUse1, so there is no key left to box into. This is not a theory: the
Ascend write-up records both
libtorch_npufallback and dispatch interception as measured and closed off.
For Kunlun specifically, this gate decides the whole route and its answer is not yet known in this repo. It must be measured on a real P800 with the real vendor SDK before either operator command is picked. Do not infer it from marketing material about CUDA source compatibility — source compatibility of kernel code says nothing about which dispatch key the vendor's torch registers.
The extraction target is the vendor's torch install (or wheel), not an NVIDIA wheel:
- Locate
libtorch_cuda.so+libc10_cuda.soin the vendor'storch/lib. - Verify the exact version match against the branch's torch version.
Constraint 3 of the CUDA doc: versions must match exactly. A vendor torch
built on 2.9.0 pairs only with the
2.9branch — this is precisely why command 1 exists as a separate axis, and why the branch is chosen first. - Stage into the assets dir and bundle.
setup.py::_bundle_cuda_assets()already implements this, gated onFLAGOS_ACCELERATOR == "cuda"and readingFLAGOS_CUDA_ASSETS_DIR. Extending that gate to accept a vendor selector is the code change this command needs — the copy logic itself is done, including the size-comparison skip that avoids re-copying ~1GB. - Record the provenance: vendor SDK version, torch version,
.sosizes and hashes. A bundled 1GB binary of unclear origin is a supply-chain problem, and redistribution terms for a vendor.soare a licensing question for a human, not for the agent. The harness stages and documents; it does not decide.
Populate backends_<vendor>.conf from backends_cuda.conf, then narrow it by
measurement. Vendor CUDA-compatibility is rarely total; the honest output is a
conf where each entry was tested, and unsupported ops fall back explicitly.
- vendor
.soversion matches the branch torch version exactly import torch_flclean throughwith_cuda_libtorch.shtorch.__version__still+cpu— no vendor CUDA torch installed into the envtests/integration/ops/passing for routed ops, with failures either fixed or removed from the conf, never left silently routed- provenance recorded in
docs/vendors/<vendor>/installation.md
The first CUDA op in a fresh process can hit Allocator not initialized for device, because this scheme deliberately never calls
torch.cuda._lazy_init(). It surfaces on out-variants (mm.out, bmm.out) run
as the very first op. It is an artifact of the external-libtorch scheme, not a
defect in the port; do not "fix" it by reaching into torch.cuda internals,
which is exactly what the scheme avoids.
Axis: hardware. Precondition: command 2 passed and command 3's gate returned "not CUDA-compatible".
Bind the vendor's own operator library per operator. Each kernel marshals its arguments, allocates and shape-infers its own output, and invokes the vendor call. This is the Ascend/GCU/MUSA route.
/native-op-backend kunlun --op-lib libxdnn.so --category unaryThe Ascend aclnn codegen is a working precedent and its architecture transfers directly. Three properties matter:
- Dispatcher declarations are reused, not re-declared.
generated/ops.halready has theXxxFntypedefs andDECLARE_DISPATCHER;ops.cchasADD_IMPL_TO_DISPATCHER;register.incbinds the ATen op. A native backend emits onlyREGISTER_IMPL_TO_DISPATCHER(XxxFn, xxx_dispatcher, Backend::k<Vendor>, XxxKernel<Vendor>). - Symbol names must agree with the ATen codegen. Reuse
codegen_ops.py:schema_to_cpp_name()or the link fails. - Category-driven, not operator-driven. The vendor calling convention is uniform; the real variation is argument marshalling and output allocation, which is consistent within a category. Ascend reached 63 categories covering 138 operators this way. Expand category by category, verifying each against CPU on real hardware before starting the next.
The critical asymmetry versus command 3: the ATen schema does not carry the vendor API name or the marshalling rules. A native backend therefore needs a hand-maintained mapping table, and the agent's job is to propose entries and verify them, not to guess them. Record: the two-stage vs single-call convention, the tensor/scalar/int-array wrapper types, the dtype enum mapping, and how workspace is requested.
Generate into csrc/aten/backends/<vendor>/generated/<vendor>_kernels.cc.
Hand-written and generated registrations are mutually exclusive — a duplicate
registration for the same backend slot is a hard error at import — so the
generator reads a skip list. The Ascend principle applies: anything a category
can express goes through codegen; hand-writing is reserved for what codegen
cannot express.
Add op = <vendor> lines to backends_<vendor>.conf only for operators that
have passed a CPU-comparison test on real hardware. Unrouted operators reach
cpu_fallback.
- each claimed operator has a recorded max-error against CPU on real hardware (the Ascend bar: unary ≤4.4e-5, binary ≤4.7e-6, comparisons exact)
- no duplicate backend-slot registration
- conf regeneration idempotent
docs/reference/operator-support.mdupdated from measured results, per AGENTS.md's operator-support rule — routing configuration is not evidence
The sequence is fixed by the dependency graph, and the branching decision sits in the middle where it can be made from evidence:
/torch-version-port 2.9— create the2.9branch, pintorch==2.9.x+cpu, regenerate ATen bindings. No P800 needed; can start immediately./runtime-bringup kunlun --sdk-path <XRE>— the 38-function floor. Needs the SDK; needs a P800 only for the final round-trip check.- Run command 3's Phase 3a gate on real hardware. Dump the dispatch table of the vendor's torch. This is the one unknown that determines everything after it, and it is cheap to answer once hardware is in hand.
- Then either
/cuda-compat-vendor kunlunor/native-op-backend kunlun.
Two things block on access rather than on design: the XRE/XDNN SDK, and a P800 for the gate in step 3. Steps 1 and 2a can proceed without either — the reconnaissance phase is read-only over headers.
cuda-op-integrationsplits: version-port concerns move to command 1, the external-libtorch and boxing concerns become command 3. The skill as written conflates them, which is why it reads as CUDA-only despite most of its content being version-generic.pre-pr-checksis unchanged and remains the final step of all four commands.- The four commands share the reconnaissance-report convention
(
docs/vendors/<vendor>/*-api-map.md): a committed, human-reviewable artifact produced before any code is generated. It is the review surface — reviewing a mapping table is tractable, reviewing 138 generated kernels is not.
- Recon is read-only and its output is committed. Generation never happens in the same step as discovery.
- Gaps are recorded, never stubbed. A missing SDK slot is documented as a
capability gap. A stub returning
Successfor an operation that did not happen is worse than a link error. - Support claims come from measurement. Every "supported" entry traces to a test run on real hardware, per AGENTS.md. Routing config is not evidence.
- Idempotency is a gate, not a nicety. Every generator's second run must produce no diff.
- The agent stages, humans decide. Redistributing a vendor
.so, promoting a platform's status, merging a branch — the harness prepares these with provenance and stops.