Skip to content

Commit 923de18

Browse files
docs(plan): the four general build-infrastructure gaps (#583)
* docs(plan): the four general build-infrastructure gaps Separates what is general build infrastructure from what belongs to the heterogeneous domain, by a stated criterion: an item qualifies when at least two of CMake / Meson / Autotools / Cargo have a counterpart and its reason for existing names no domain concept. RDC fails that test and is left to docs/20; the primitive it would reuse already exists. Four gaps survive, and two of them were smaller than they first looked once main was measured rather than recalled: * config.h generation is NOT an engine gap. All four pieces are present (toolchain_dir/sysroot_dir for the right compiler, a real program to write the file, include-dir to make the package's TUs see it, rerun-if-changed for incrementality). What is missing is a shared probe library, which is a package rather than an engine change. * package layout does NOT need a new section. `[runtime].artifacts` already declares a relative path plus a role; the white list is missing exactly one role -- a data file read by a loader outside this package. Adding a section would have duplicated an answer another section already gives, which docs/05 Appendix A refuses. The two that remain are an `exports` declaration rendered per platform (one neutral statement, three renderings, the same shape `[runtime]` already established) and a generic `link-flag` directive, which is the member the link-lib / link-search / link-script family is missing and the escape hatch a generated version script needs. Every criterion is two-sided, and C6 is the one that cannot be verified on a developer machine: a probe implementation that wrongly reads the host is green wherever /usr/bin/cc exists, so it has to run in the hermetic container job. * docs(plan): attribution, the programming-model three-way split, and a falsification target Revises the design doc. Two of its own judgements were wrong and are corrected in place rather than appended to, because a design doc whose corrections live at the bottom is read top-down and gets the retracted version. * RDC is plugin-side, engine change zero. Rechecked against the attribution rule this revision adds: it names a vendor (test 1), changes no artifact property (test 2) and invents no edge kind (test 3). Every primitive it needs exists -- `action` with the `object` destination is "an external step's object joins the ordinary link", twice. It therefore does not sit on the critical path, which changes the staging. * stdpar is islandable, so calling it mutually exclusive with mcpp's model was an overclaim. `-stdpar` decomposes into a compile-side island and a link-side allocator, and the allocator is a whole-image property imposed by a dependency -- the shape `cxx_runtime` already established. What is lost is the selling point ("change no source"), not the capability. docs/20's two-way split should become three-way. Three gaps added. The first came out of a manifest that reads badly: [target.'cfg(not(any(accelerator = "cuda", accelerator = "vulkan")))'.build] `accelerator` is an open vocabulary by design, and `not(any(<enumeration>))` over an open vocabulary silently changes meaning as the ecosystem grows -- a fifth backend edits the meaning of every fallback predicate already written. The general rule is that an open vocabulary needs a "none" that does not enumerate, and the spelling is already in this manifest: `os = "none"` is bare metal, so `accelerator = "none"` is no device backend. Preferred over `cpu`, which would make the axis carry two questions and leave the truth value of `cfg(accelerator = "cpu")` under `accel = "cuda"` undecided. The other two are the package-wide barrier a generated header creates (`source`'s documented semantics: every compile edge of the package waits) and Fortran, which is recorded as identified and deliberately given no design. Also: an attribution rule (three ordered tests) so the engine/plugin boundary is decided rather than argued each time; a note that "island" collides with branch-island in linker vocabulary and cuts across single-source/separate- source, with an alignment sentence rather than a rename; and CANN as the experiment that could falsify the central claim, since all four existing lanes are in one vendor lineage. Its decisive criterion is `git diff src/` empty. * docs(plan): the three CANN uncertainties, measured Two confirmed, one not, and the one that failed is the only real obstacle. All CANN repos clone anonymously from gitcode.com/cann at branch 8.5.0 under the CANN Open Software License Agreement 2.0. CONFIRMED -- hardware-free execution exists, and it is the vendor's own test path. Ascend C has three run modes, not two: npu (default), sim (NPU simulation) and cpu (CPU debug), selected by -DCMAKE_ASC_RUN_MODE=. The simulators ship inside the toolkit per SoC and asc-devkit's own unit tests link them (pvmodel_ascend910/310p/610, pem_davinci_ascend910B1/310B/610Lite). The distinction that decides whether a criterion is usable: cpu mode links tikicpulib and the tikcpp headers, so the same kernel source is compiled by the HOST compiler and there is no island in that graph at all. Passing in cpu mode proves the kernel's numerics, not the mechanism under test -- the criterion would be pointed at the wrong object, which is a shape this repository has paid for repeatedly. sim mode is recorded as keeping the island by inference, not by measurement, with the check to run at implementation time stated. CONFIRMED -- operator registration is JSON and is per-SoC. In ops-math (72 MB, 1451 .cpp under math/) each operator carries op_host/config/<soc>/<op>_binary.json mapping the operator signature to the device binary's filename, with six SoC directories. That is a textbook role = "manifest" case and it is target- conditional, so the slice exercises SPEC-004's target axis as well. The source layout is also already islanded: op_kernel/ beside op_host/ per operator. NOT CONFIRMED -- BiSheng is not a standalone artifact. It lives at ${ASCEND_DIR}/compiler/ccec_compiler/bin/bisheng inside the CANN toolkit, whose download requires accepting a separate licence and appears to need login. No stable anonymous URL was found and redistribution terms are unverified. This is the experiment's one real obstacle and it is not a technical one. Consequently the feasibility gate is corrected: "hardware or a simulator" was wrong as a hard gate, because the decisive criterion (git diff src/ empty) is entirely build-time. The only true prerequisite is obtaining BiSheng lawfully. Incidental finding worth recording: CMake registers Ascend C as a LANGUAGE (FindASC.cmake, "plugin support ASC language") with the real machinery inside the toolkit. That is third-party corroboration of the axis in section 9 -- CMake puts it in the engine, mcpp puts it in a package. Same axis, different attribution, which is exactly what section 7 exists to decide. * docs(plan): the toolkit pulls anonymously, so the experiment has no blocker Overturns the previous commit's one negative finding, and corrects the framing error underneath it. The framing error first: BiSheng and the simulator were treated as two acquisition questions. They are one -- ccec_compiler/bin/bisheng and simulator/<SoC>/lib are both inside the CANN toolkit. The toolkit's official distribution is a Docker image, not a .run installer (measured in ops-math/QUICKSTART.md): swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:8.5.0-910b-ubuntu22.04-py3.10-ops and it pulls anonymously. Measured: GET /v2/ 401 GET /swr/auth/v2/registry/auth?...:pull token issued GET /v2/ascendhub/cann/manifests/<tag> with token 200, arm64 + amd64 That 401 is what produced the wrong answer the first time. A bare 401 reads identically to "credentials required", but it is also the first step of an anonymous token handshake -- Docker Hub behaves the same way. Stopping at step one turns "anonymously available" into "unobtainable". The criterion has to walk the whole handshake, which is this repository's recurring lesson about a criterion whose "no" and whose "not measured" produce the same reading. Compliance lands in the tier the invariant already allows: proprietary vendor userspace is fetched from the vendor's own published URL and never copied into an xlings-res release. Pulling the official image from Huawei's own registry is exactly that, so no redistribution right is needed. Also records that AscendNPU IR is now open with a Triton path (Triton IR -> Linalg -> AscendNPU IR), which gives a rule package a second possible entry height. Not selected -- Ascend C remains the direct one, because it is the actual form of the 1451 .cpp files in ops-math. The feasibility gate therefore has no blocking row. One technical question remains before implementation: whether sim mode still invokes bisheng, since that decides which object the device criterion is pointed at. * docs(plan): sim mode does invoke bisheng, measured in vendored cmake Closes the last open question in section 10. It was recorded as inference; it is now source. asc-devkit vendors the toolkit's own ASC_CMake under tools/ascc/cmake/, so the device compile path is readable without pulling the image: CMakeDetermineASCCompiler.cmake:47 find_program(CMAKE_ASC_COMPILER NAMES "bisheng" PATHS ".../ccec_compiler/bin/") CMakeASCInformation.cmake:49 CMAKE_ASC_COMPILE_OBJECT = "<CMAKE_ASC_COMPILER> ... -c -x asc <SOURCE>" host_config.cmake:69 CCEC_LINKER = <toolkit>/ccec_compiler/bin/ld.lld The decisive one is negative evidence: every RUN_MODE test in the repository is STREQUAL "cpu". There is no sim branch anywhere. Build-time therefore distinguishes only cpu from not-cpu, so sim takes the same path as npu and bisheng is invoked. The two differ at run time -- which runtime and simulator libraries load -- not in the build graph. The one extra action on the non-cpu branch is update_host_stub.py, which generates host-side launch stubs, and a launch stub exists precisely because there is a real device binary to start. Worth keeping the reason the question was asked: the opposite design is real and has good reasons behind it, since instruction-level simulation is orders of magnitude slower than host code and a host compiler gives gdb and ASAN. CANN split those concerns instead -- cpu mode IS that design, so sim would duplicate it unless it executed real device instructions. Three modes rather than two is itself the answer. Section 10 now has no open items. * docs(plan): the validation item gets a criterion and a slot, and that criterion was wrong Review pass over the whole document. Three findings, one of which is a defect in a criterion rather than an omission. OMISSION -- the validation experiment had neither a criterion nor a stage. It was described at length in section 10 and then appeared in neither the criteria table nor the staging table, so nothing tied it to the rest of the plan. Added as C9 and as a staging row with 13.1 explaining where it sits. DEFECT -- C9's decisive criterion, "git diff src/ is empty", cannot tell two failures apart: * the diff carries an Ascend-specific identifier -> the claim is refuted * the diff is a general capability like exports -> the engine is merely missing general infrastructure, which is what sections 2-6 exist to add Both read the same, so running it before the general gaps land would report the second as the first. Replaced by two levels: the primary criterion is that test_core_vendor_probes.cpp stays green after the port, which judges by property rather than by whether anything changed and therefore needs no precondition; "git diff src/ empty" is kept as a stricter additional statement, valid only after stages one to three. That also unblocks running the experiment early: the primary criterion works at any time, and an early run may surface a fifth general gap, which is cheaper than discovering it after three stages of engine work. ORPHAN -- kind = "device" appeared once, in the attribution table, with no criterion and no stage. It sits in a cross box: engine-side by test 2, but domain by the section 0.1 admission line, because it talks about accel. Two rulers, and they cross. Stated explicitly and scoped to docs/20 rather than left ambiguous. Also marked the superseded half of revision two in the change record, so a reader scanning the log does not take an overturned conclusion as current. * feat: link-flag, and an open vocabulary that can say "empty" Stage one of the general build-infrastructure design (.agents/docs/2026-09-07-general-build-infrastructure-gaps-design.md). mcpp:link-flag (protocol v8). link-lib, link-search and link-script each name one kind of thing, so a flag the program COMPUTED had no outlet: a version script whose content depends on which features are on, -Wl,--wrap=malloc for a runtime that takes over a C-library symbol, -Wl,--exclude-libs,ALL so a statically absorbed third party does not become part of this package's ABI. It reaches the consumer, and that CORRECTS the design doc, which first ruled it private by analogy with include-dir. The analogy is false and the code says so: linkUsage.ldflags is a copy of buildConfig.ldflags, so a private link flag is not a policy this engine can express, and [build] ldflags -- the declarative twin -- already propagates. Making the computed form behave differently would be the inconsistency rather than the safeguard. The hazard that follows is stated in the docs rather than hidden: a dependency emitting --version-script puts it on the consumer's link too, which a dependency writing the same flag in [build] ldflags has always done. cfg(accelerator = "none"). A CPU fallback could only be written by enumerating the backends it is not, and accelerator's vocabulary is open by design -- so that predicate changes meaning the day a fifth backend exists, and every fallback already written starts treating a build that named the new backend as having no accelerator. The spelling reuses os = "none", which this manifest already means "bare metal" by. Not cpu: that puts a second question on an axis whose job is "which device compiler, which architecture", and leaves cfg(accelerator = "cpu") under accel = "cuda" with no self-consistent answer. Both criteria measure the property rather than its shadow. e2e 620 asserts the LINKER'S BEHAVIOUR -- the program computes -Wl,--defsym=mcpp_e2e_620=42 and the artifact prints that symbol's address -- because grepping build.ninja would pass for a flag written down and never handed to the linker. The unit test SIMULATES the fifth backend arriving: the enumeration starts lying on the spot and none does not, which is the entire reason the row exists and is invisible under a single backend. * feat: exports -- one neutral statement of the symbol surface, three renderings Stage two of the general build-infrastructure design. Both platforms already publish everything: ELF gives symbols default visibility, and PE gets an auto-generated .def listing every symbol (mcpp.build.coff_exports, WINDOWS_EXPORT_ALL_SYMBOLS semantics). What was missing is the other direction. A runtime with a stable ABI publishes a reviewed set so that what is outside it stays free to change; a plugin loaded beside its rivals must not collide -- a Vulkan ICD that exports its internals collides with the loader and with the other ICDs in the process. This repository has the symptom on file: mcpp's own duplicate-symbol check on the SYCL example reports 68 _Unwind_* symbols, because one image holds two C++ runtimes and both export them. exports takes a file of symbol patterns or an inline list, and the backend renders it per platform -- version script, -exported_symbols_list, or the .def that replaces the all-exports one. One statement, three renderings, which is the shape [runtime] already established and the reason this is a manifest key rather than three platform-specific flag lists. IT DOES NOT IMPLY HIDDEN VISIBILITY, and that CORRECTS the design doc, which said it should. The narrowing is a link-time property on all three formats, so implying a compile-time one would give a single key two effects -- and the second effect also changes how this library's own translation units see each other, which is a separate decision with a separate reason. -fvisibility=hidden stays available through [build] cxxflags for the code generation it buys. The export list is read at manifest load rather than at plan time, so every later stage sees one representation; origin's parent is the package root and that holds for the root, a path dependency and a store dependency alike, which is the "same decision in N places" this would otherwise become. e2e 621 builds one source twice and requires the two readings to DIFFER. Asserting only that the public symbol is present passes for a library that exports everything, which is the state before this change; asserting only that the internal one is absent cannot distinguish "correctly hidden" from "never linked at all". * docs(plan): stages one and two landed, and three judgements they corrected Records what implementation established, including where it contradicted the design. * link-flag reaches the consumer. The doc ruled it private by analogy with include-dir; the code refutes the analogy. linkUsage.ldflags is a copy of buildConfig.ldflags and propagateLinkFlags pushes every dependency ldflag to the consumer, so a private link flag is not a policy the engine can express. include-dir is private because a compile interface HAS a declarative public counterpart; link flags do not, so the computed form must behave like its declarative twin. C4 is rewritten accordingly. * exports does not imply hidden visibility. The narrowing is link-time on all three formats, so implying a compile-time effect gives one key two, and the second changes how the library's own TUs see each other -- a separate decision. C3 is retired because it asserted exactly that coupling. * Section 4.3's open question is answered. manifest_emit builds [[runtime.artifacts]] from doc.legs plus one interface entry and does NOT carry the author's declared artifacts; role is a free string in the parser and the white list is an effect of its readers. So stage three is a new path through the packer -- carry a declared file into the artifact -- not a new role value. Staging is unchanged; its rationale is. * docs(examples): a multi-backend artifact, an Ascend one, and the table a rule package belongs in Two examples, and one correction that applies to every example already here. multi-backend -- several backends in ONE artifact, chosen at run time. The four examples beside it are each one seam: a device file and a CPU file define the same symbol and are never in one link, so exactly one exists and the choice is made at build time. That is right for a program. A library cannot make that choice; it is compiled once and consumed by people whose machines differ, so its backends are additive. It is also the first real use of cfg(accelerator = "none"). The dispatcher must be built for cuda, for vulkan, for both, and for a backend that does not exist yet, and not(any(cuda, vulkan)) needs editing every time the ecosystem grows -- the edit that is forgotten being silent. Its default accel is empty, so a plain mcpp build needs no payloads at all, which is what lets CI build it and makes the "none" path genuinely covered rather than described. cann -- Ascend, behind the same seam. It DOES NOT BUILD, and the README says so and names the two missing pieces: a rules-ascendc rule package, and an xim package for the toolkit. It records what was measured instead of guessed: both BiSheng and the simulator live in that one toolkit; the toolkit image pulls anonymously; sim mode needs no hardware AND keeps the island, while cpu mode does not keep it and therefore cannot stand in for a device criterion. The manifest is written out rather than described so the shape is concrete. CANN's own operator libraries already split op_kernel/ from op_host/, so the island is not a shape mcpp imposes on Ascend. THE CORRECTION. All four existing examples declare mcpp:plugins in [dependencies], and docs/05 section 2.6.1 names exactly that case as what [build-dependencies] is for: a package whose library must never reach the target while its rule is still wanted. The two axes are separate -- host-module = true says which build-time product is wanted, the section says whether the package reaches the target, and a rule package answers no on the second. Verified by moving it and rebuilding: build.mcpp compiles, the rule runs, shaders compile, and the artifact runs on a real device. Writing it in [dependencies] still works, which is precisely why the distinction has to be stated rather than left to a failure to teach. * Five defects a second backend found, and none of them was findable by reading The gap list this branch implements came from reading the code and from one device backend. Writing the second one turned up five more in an afternoon, and not one of them could have been found by continuing to read: every reading is either "the build succeeded" or an error message pointing somewhere else. A DEVICE SOURCE CAN COMPILE NOTHING, AND THE BUILD SUCCEEDS. A device-kind file is the one source the engine has no rule for: it goes to the package's build program and comes back as an action, or it is never compiled -- and nothing checked that it came back. The multi-backend example had no build.mcpp at all, so both device globs were dropped in silence and the reading was `undefined reference to opkit_cuda_saxpy` at the link: a message naming a symbol and never the file that would have defined it. A `kind = "lib"` target does not even get that, because an archive is not resolved. Now refused, naming the files, and distinguishing "no build program" from "a program that claimed nothing" because the fixes differ. The criterion is the action INPUTS, which is also the condition an action needs anyway: one that compiles a file it does not declare as an input does not rerun when that file changes. ONE RULE TOOK EVERY DEVICE SOURCE. `device_sources()` is the package's whole set and every rule in a build program reads it. Correct for exactly as long as a build has one rule in it -- a premise never written down. Measured with two: the CUDA rule compiled `scale.comp` AS CUDA and produced an object, and the shader rule then failed on the `.cu` with a message about stages. The louder failure was the harmless one. Fixed in mcpp-plugins 0.2.2 (each rule claims its extensions); the engine's half is the refusal above, because "no rule claimed it" is only visible here. `accelerator` WAS CLASSIFIED AS A RESOLVED LAYER. The five real layers are answered by dependency resolution, so refusing them in `[xlings]` predicates is right: payloads are installed before resolution. The accelerator is not one of them -- it is `--accel`, or `[build] accel`, read before the first package is looked up. The cost was paid on every build with a device island and paid worst on the cheapest one: a vendor toolkit could be declared unconditionally or not at all, so a CPU-only build downloaded gigabytes for a device it was not compiling for. Split by SCHEDULE rather than by subject; the connected consequence is that `[target.'cfg(accelerator = "cuda")'.dependencies]` now applies, there being nothing circular about it. NAMING A SUBSET OF BACKENDS WAS TREATED AS A MISMATCH. The refusal is right about architectures -- a file for sm_89 in a build targeting sm_80 is not a variant -- and was applied across backends, so a package with a CUDA island and a Vulkan one was refused when built with `--accel vulkan1.2`. A package could have several device backends only if every build took all of them, which is exactly what an additive-backend library cannot do. A glob whose backend is not named is now left out as `--no-accel` leaves it; what keeps that from turning `accel = "cude12.9"` into a file that is never compiled and never mentioned is a new check against `[package] accelerators`. A RULE'S PAYLOAD WAS UNREACHABLE FROM THE CONSUMER. A rule's code runs inside its consumer's build program, so `xpkg_dir` is asked there while the payload was declared in the rule's own `[feature-xlings]`. The graph already installed it; only the answer was missing, because `fillXpkgDirs` read one manifest. The reading was "the toolkit is not installed" with the toolkit on disk. Also: an import no dependency provides is refused by name. Left to the compiler it is `failed to read compiled module` plus a note that imports must be built first -- true, and naming neither the package nor `host-module = true`. The set of names that can compile there is closed, so a name outside it is refused with the candidates that could have provided it. VERIFICATION. e2e 622 through 626, five files. 622 and 625 were run against the released 2026.9.6.4 and failed as designed. 626 has a negative leg because two of its three legs would pass on an engine that refused every device source. 107 unit tests pass; the two cfg tests that stated the old classification now state the new one. examples/09-heterogeneous/multi-backend is the example all of this came from, and it now works: four command lines, three of them on a real RTX 4080. mcpp run backend: cpu (only backend in this build) mcpp run --accel "vulkan1.2" backend: vulkan (NVIDIA GeForce RTX 4080) mcpp run --accel "cuda12.9+{sm_89}" backend: cuda mcpp run --accel "cuda12.9+{sm_89}, vulkan1.2" backend: cuda Its Vulkan half was a stub that declined; a stub would have printed `backend: cpu` for a Vulkan build, which reads as "Vulkan failed here" rather than "this was never written", so the real implementation is ported from the sibling example. Its CUDA leg takes the clang route, measured rather than chosen: on the 12.9 line nvcc's own front end refuses the toolkit's non-`noexcept` `cospi` against the C library's, an older `xim:gcc` payload does not help because the declarations come from the C library, and the 13.x line raises the driver floor to r580 -- a requirement on the machine rather than a decision the project makes. * `.asc` and `.cce` enter the device-source table, and the CANN toolkit turns out to be one download The Ascend example named an `xim:cann-toolkit` that did not exist and a device extension the engine did not know. Both are now answerable, and the answer to the second was simpler than the design doc said. THE EXTENSION. `.asc` is Ascend C, compiled by `ccec` (BiSheng) from the CANN toolkit, and `.cce` is the older spelling of the same thing. They belong in the table for the reason `.sycl` does: the content is ordinary C++ and the criterion is the compiler. CANN's own operator libraries already split `op_kernel/` from `op_host/`, and CMake registers ASC as a language of its own -- the island is a shape Ascend already has, not one mcpp imposes on it. Adding to this table cannot change a build that works today: device extensions are absent from the default source glob, and one named in `sources` was until now a hard error. THE TOOLKIT. The design doc recorded that the vendor's container image pulls anonymously, which is true and is not the simplest route -- it stopped one layer short. Measured today, with no credentials at all: every toolkit from 8.0.RC1 to 8.5.0 is a plain `.run` on Huawei's own OBS, answering 200 to a HEAD request. 8.5.0 is 1.12 GB for x86_64 and 1.10 GB for aarch64. It installs without root and without a driver: ./Ascend-cann-toolkit_8.5.0_linux-x86_64.run --install \ --install-path=<dir> --quiet and 2.9 GB later the two pieces the lane needs are both there: `x86_64-linux/ccec_compiler/bin/{ccec,bisheng}` runs and reports clang 15.0.5, and `x86_64-linux/simulator/` carries 38 SoC directories, each with its own `libpem_davinci.so`. So the whole lane -- compiler and a hardware-free way to run what it produces -- is one anonymous download. The installer writes two things outside its install path, `~/Ascend` (8 KB, an install record) and `~/var/log/ascend_seclog`. Measured: overriding `HOME` for the duration contains both, and the payload installs identically, so a package recipe need not be a bad citizen of the user's home directory. This settles what the design doc listed as the lane's only remaining prerequisite. Compliance-wise it sits in the first tier of the standing rule -- closed-source and not redistributable, so it is fetched from the vendor's own URL with no CN mirror, which costs nothing here because the vendor's URL is already in-country. Verified after the mcpp-plugins 0.2.2 release: the multi-backend example now resolves the PUBLISHED `mcpp:plugins@0.2.2` from the index with no path override and no lock file, and runs -- `backend: cpu` with no accelerator and `backend: cuda` on a real device. * The first thing the new refusal caught was this repository's own fixtures Three e2e fixtures built a `build.mcpp` that read `mcpp::device_sources()`, printed it, and compiled nothing. They were asserting on the env contract, which is a real thing to assert on -- and they were modelling a project whose device files compile to nothing, which is exactly the defect the new refusal exists to catch. So the refusal caught them, and it was right to. 606, 609 and 613 now declare a `check` action per device source, which is the edge a real rule package declares, and go on asserting on the variable exactly as before. The action's input has to be ABSOLUTE: `device_sources()` is package-root-relative and an action does not run in the package root, which the first attempt got wrong and `cp: cannot stat 'shaders/s.tesc'` said plainly. EXAMPLE 05 PINS ITS TOOLCHAIN, and this is a different failure that surfaced in the same run. `mcpp pack` names its output directory after the ABI tag, and the consumer beside it names that directory literally -- so the pair only agrees on a machine whose DEFAULT toolchain is the one that produced the tag. Measured: a runner whose sandbox had been left with an LLVM default produced `mathkit-0.1.0-x86_64-linux-gnu-clang22-libcxx22-c++23` while the consumer named the `gcc16-libstdcxx16` one, and the example that had passed for months failed without anything in it changing. An example that hard-codes an ABI tag has to name the toolchain that produces it. That is not a workaround for CI: a packed artifact IS specific to the ABI it was built against, which is the entire subject of that example, so leaving the toolchain implicit was the inconsistency. * The Ascend example stops being a sketch: the kernel compiles, and the driver is what is missing It was written as a shape with two named gaps -- a rule package and an xim package for the toolkit -- and both now exist. What replaced them is a list of measurements. On an x86_64 machine with no Ascend hardware and no Ascend driver: `xim:cann-toolkit` provisions 2.9 GB without root; `build.mcpp` compiles and imports `mcpp.rules.ascendc`; the kernel compiles under `bisheng -x asc --cce-aicore-arch=dav-c220`; the resulting object joins the ORDINARY link, because the rule compiles in mixed mode and gets an x86-64 object carrying the device binary rather than a Da Vinci one the host linker cannot place; the host half links against ACL and the six-library closure the rule names. The artifact then does not start, because `libascend_hal.so` is missing -- the DRIVER, which is the role `libcuda.so.1` plays for CUDA: in ABI lockstep with the kernel module, not redistributable, and correctly absent here. `mcpp run --no-accel` builds and runs: `12 24 36 48`, `device: cpu`. So the example completes everywhere and RUNS only on an Ascend machine, which is the same statement `examples/09-heterogeneous/cuda` makes about a machine with no NVIDIA driver. Its skip reason says that instead of what it used to say. The payloads are gated on the accelerator, so the CPU leg installs nothing at all -- which is the first real use of that gating outside the multi-backend example. THE SEAM IS A C FUNCTION, and that is measured rather than stylistic: BiSheng's own launcher for a `__global__` function is C++-MANGLED even when the kernel is declared `extern "C"`. Calling it directly would make the program depend on BiSheng and the project's C++ compiler agreeing about mangling -- clang 15 and whatever the project chose. The `.asc` file exports an `extern "C"` wrapper instead, and the `<<<...>>>` launch spelling never leaves the translation unit the device compiler owns. The README's "what was established about the toolkit" section is replaced by what the toolkit turned out to BE, including the 38 SoC simulators that make this lane verifiable without an NPU -- which is the next thing this example should use, and a separate piece of work with its own contract. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 325e5ff commit 923de18

65 files changed

Lines changed: 3944 additions & 76 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/docs/2026-09-07-general-build-infrastructure-gaps-design.md

Lines changed: 895 additions & 0 deletions
Large diffs are not rendered by default.

.github/tools/build_examples.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ BUILD=(
2424
examples/03-pack-static
2525
examples/04-workspace
2626
examples/08-build-rules/app
27+
# The CPU-only path of the multi-backend example: no payloads, and it is
28+
# where `cfg(accelerator = "none")` is exercised. The device paths are
29+
# opt-in via --accel and are covered by the rule packages' own CI.
30+
# Built here for one reason worth the cost: it is the only example whose
31+
# CPU-only configuration exercises `cfg(accelerator = "none")` and two rule
32+
# packages in one build program, and both of those are engine paths that a
33+
# description cannot cover. Its `[toolchain] default = "llvm@22.1.8"` means
34+
# this job installs an LLVM payload it otherwise would not -- the CUDA leg
35+
# takes the clang route, because the nvcc route on the 12.9 line is refused
36+
# by nvcc's own front end and the 13.x line raises the driver floor to r580.
37+
# The device payloads are NOT installed: they are gated on the accelerator,
38+
# and this builds without one.
39+
examples/09-heterogeneous/multi-backend
2740
)
2841

2942
# `key|reason`.
@@ -38,6 +51,7 @@ SKIP=(
3851
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
3952
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
4053
"examples/09-heterogeneous/vulkan/app|built AND RUN by the next step of this job, on the lavapipe payload, which needs no GPU"
54+
"examples/09-heterogeneous/cann/app|its device leg needs the Ascend DRIVER, which a runner does not have: the kernel compiles and the object links, and then `libascend_hal.so` is missing, which is correct on a machine with no NPU. Its CPU leg does build -- and is not built here only because the plugins pin would make this job resolve a fifth rule package for one example. Covered by the measurements in its README"
4155
)
4256

4357
# Every ROOT manifest in the tree: a directory with an `mcpp.toml` that has no

CHANGELOG.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,128 @@
55
66
## [Unreleased]
77

8+
### 写第二个后端的时候,才发现接口只对第一个成立
9+
10+
多后端示例本身没建起来,而它暴露的四件事都不是示例的问题。
11+
12+
**设备源可以什么都没编到,而构建成功。** 设备类源是引擎唯一没有编译规则的源:它交给
13+
本包的构建程序,要么作为 action 回来,要么根本不被编译 —— 而**没有任何东西在检查它
14+
回来了**。示例没有 `build.mcpp`,于是两个设备 glob 被静默丢掉,读数是链接期
15+
`undefined reference to opkit_cuda_saxpy`:那条消息点的是符号,从不是那个本该定义它的
16+
文件;`kind = "lib"` 的目标连这条都没有,因为静态库不做解析。现在拒绝,并点名文件,
17+
且区分「根本没有构建程序」与「程序跑了但没有 action 认领它们」——两者的修法不同。
18+
判据取 action 的**输入**,这同时是 action 本就该满足的条件:编译某文件却不声明它为
19+
输入的 action,在该文件变化时不会重跑。
20+
21+
**规则声明的载荷,消费者的构建程序够不到。** 规则的代码跑在**消费者的**构建程序里,
22+
所以 `mcpp::xpkg_dir` 是在那边被问的,而载荷是规则在自己的 `[feature-xlings]` 里声明
23+
的。依赖图早就会安装它;缺的只是回答 —— `fillXpkgDirs` 只读一份 manifest,于是地址被
24+
下载、解包,然后对唯一想用它的那段代码不可见,读数是「工具包没装」而它就在盘上。
25+
26+
**`accelerator` 被当成了「解析出来的层」。** 五个真正的层(`c-abi``compiler` …)由
27+
依赖解析回答,所以以它们为谓词的 `[xlings]` 表被拒绝是对的。`accelerator` 不是:它是
28+
`--accel`,或 `[build] accel`,在查找第一个包之前就已读入。混在一起的代价每次构建都在
29+
付 —— 厂商工具包只能无条件声明或者不声明,于是**不带加速器的那次构建**(最便宜的、
30+
也是 CI 跑的那次)会为一个它没在编译的设备下载数 GB。现在
31+
`[target.'cfg(accelerator = "cuda")'.xlings.workspace]` 与同谓词下的
32+
`[dependencies]` 都生效。
33+
34+
**忘了 `host-module = true`,消息里没有这个词。** 实测读数是 GCC 的
35+
`failed to read compiled module` 加一句「imports 必须先被构建」—— 都对,而既没点出会
36+
提供这个模块的包,也没点出那个让它可导入的键。构建程序能编译的名字是一个闭集
37+
(`std``std.compat`、内置 `mcpp`、可导入的 host 模块),所以集合之外的名字在编译器
38+
被调用**之前**就被拒绝,并列出声明了却没写 `host-module = true` 的依赖。
39+
40+
配套判据:e2e 622(规则载荷跨到消费者)、623(设备源三条腿,含 `--no-accel` 的反向腿)、
41+
624(点名拒绝,且它给的修法真的能修好)、625(按加速器开关载荷,两个方向)。622 与 625
42+
都在已发布的 2026.9.6.4 上跑过对照并如期失败。
43+
44+
### 两个新示例,以及一处所有既有示例都写错了的地方
45+
46+
**`examples/09-heterogeneous/multi-backend`** —— 多个后端进**同一个产物**,运行期选择。
47+
既有四个示例每个都是**一道接缝**:设备文件与 CPU 文件定义同一个符号、永不同时进入一次
48+
链接,所以恰好存在一个,选择在构建期做完。那是程序的正确形态。库做不了这个选择:它
49+
只编译一次,而消费者的机器各不相同,所以它的后端是**叠加**的。这个示例是那种形态。
50+
51+
它同时是 `cfg(accelerator = "none")` 的第一个真实用例:分发器要在 cuda、在 vulkan、在
52+
两者同时、以及在一个还不存在的后端下都被构建,而 `not(any(cuda, vulkan))` 每次生态新增
53+
后端都要改一遍 —— 忘掉的那次是静默的。默认 `accel` 为空,所以 `mcpp build` 不需要任何
54+
载荷,CI 因此能真正构建它,`none` 那条路径也就真的被覆盖。
55+
56+
**`examples/09-heterogeneous/cann`** —— 昇腾。**目前构建不了**,README 点明缺的两块
57+
(`mcpp.rules.ascendc` 规则包,以及承载毕昇与仿真器的 `xim:cann-toolkit`),并记下已经
58+
查实的三件事:两者在同一个工具包里;工具包镜像可匿名拉取;`sim` 模式无需硬件且保留岛,
59+
`cpu` 模式**不保留**因而不能充当设备判据。manifest 写出来而不是描述出来,是为了让
60+
形状具体。CANN 自己的算子库本来就是 `op_kernel/``op_host/` 分开的,岛不是 mcpp 强加
61+
给昇腾的形状。
62+
63+
### 规则包应当声明在 `[build-dependencies]`
64+
65+
既有四个示例都把 `mcpp:plugins` 写在 `[dependencies]` 里,而 docs/05 §2.6.1 自己立的规则
66+
恰恰点名这种情形:**库绝不该到达目标,而它的规则仍然被需要**。两条轴是分开的 ——
67+
`host-module = true` 说要哪种构建期产物,section 说这个包是否到达目标,规则包在第二条轴
68+
上答"否"。四个示例与两份文档已改正。
69+
70+
写在 `[dependencies]` 里同样能工作,这正是这条区分必须被**陈述**而不能指望由一次失败来
71+
教会的原因。
72+
73+
74+
### 共享库能说出自己发布哪些符号:`exports`
75+
76+
两个平台的默认都是"全导出":ELF 给符号默认可见性,PE 由引擎自动生成列出全部符号的
77+
`.def`**缺的是反方向** —— 声明式地只发布一组。
78+
79+
两类工程需要它。有稳定 ABI 的运行时只发布一份经评审的集合;与同类并存的插件不能撞名
80+
—— 一个把内部符号也导出的 Vulkan ICD 会与 loader 以及同进程内另一个 ICD 相撞。本仓库
81+
自己就有现成的例子:SYCL 示例构建时重复符号检查报的那 68 个 `_Unwind_*`,是一个镜像里
82+
两个 C++ 运行时都在导出 unwinder 符号。
83+
84+
`exports` 接受一个符号模式文件或一个内联数组,由引擎按平台渲染成 version script /
85+
`-exported_symbols_list` / `.def` —— 一句中立的话三种渲染,与 `[runtime]` 已确立的形状
86+
相同,而不是让作者写三份平台专用文件。
87+
88+
**它不隐含编译期 hidden。** 三种格式上收窄都是链接期属性,所以一个键只有一个效果;
89+
`-fvisibility=hidden` 仍可经 `[build] cxxflags` 取得代码生成收益,而那是单独的决定,
90+
因为它同时改变本库各 TU 之间如何看见彼此。符号**版本化**(`foo@@LIB_1.0`)不在此列,
91+
它是 ELF 独有、无法中立表达的能力。
92+
93+
判据 e2e 621 把同一份源码构建两次并要求两次读数**不同**:只断言公开符号在,会对"导出
94+
全部"同样成立(那正是本特性之前的状态);只断言内部符号不在,分不开"正确地隐藏了"与
95+
"根本没链上"。
96+
97+
98+
### 构建程序能发出它算出来的链接标志:`mcpp:link-flag`
99+
100+
`link-lib``link-search``link-script` 各自命名一类东西,于是一条**算出来的**标志无处
101+
可去:内容依赖 feature 组合的版本脚本、运行时接管 C 库符号用的 `-Wl,--wrap=malloc`
102+
以及 `-Wl,--exclude-libs,ALL`(静态吞入的第三方不得成为本包 ABI 的一部分)。签入仓库
103+
的标志一直可以走 `[build] ldflags`,生成的不行。
104+
105+
新增 `mcpp:link-flag=``mcpp::link_flag()`(协议 v8)。原样传递 —— 引擎不解析链接器
106+
词汇。**它到达消费者**,与 `[build] ldflags` 一致:编译接口有声明式公开对应物因而
107+
`include-dir` 必须私有,链接标志没有这个分裂,让"算出来"的形态与它自己的声明式孪生
108+
行为不同才是不一致。后果写明:依赖发出的 `--version-script` 也会落到消费者链接行上,
109+
而这个隐患不是新的。
110+
111+
判据是 e2e 620,它断言**链接器的行为**而不是命令行文本:程序算出
112+
`-Wl,--defsym=mcpp_e2e_620=42`,产物打印那个符号的地址。值只可能来自链接器真的收到了
113+
这条标志。
114+
115+
### `cfg(accelerator = "none")` —— 开放词表不能靠枚举取反
116+
117+
CPU 回退此前只能写成 `not(any(accelerator = "cuda", accelerator = "vulkan"))`
118+
`accelerator` 的取值是**开放的**(docs/20:第五个后端是一个包,不是引擎改动),所以这条
119+
谓词的含义会随生态增长**静默改变** —— 新增一个后端之后,每个已写好的回退谓词都开始把
120+
"命名了新后端的构建"当成"没有加速器",于是 CPU 实现与设备实现一起编进去。
121+
122+
`accelerator = "none"` 为真当且仅当加速器集合为空。拼法沿用本仓库已有的
123+
`os = "none"`(裸机),不新造词。不用 `cpu`:那会让这条轴同时承载两个问题,并且
124+
`cfg(accelerator = "cpu")``accel = "cuda"` 下的真假无法自洽地定下来。
125+
126+
判据 `test_cfg_accelerator_none.cpp` 直接**模拟第五个后端到来**:枚举写法当场开始说谎,
127+
`none` 不变。这是这项改动的全部理由,单后端下跑绿零信息量。
128+
129+
8130
### 工具也有两条解析轴:`[target.<selector>.xlings…]`
9131

10132
一条工具条目回答的是两个不同问题中的一个:它是在构建机上执行的(宿主),还是产物编译

docs/01-examples.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ examples.
3131
| 06 | [`examples/06-openkal-cross`](../examples/06-openkal-cross/) | One program asking each machine what it is, built for four targets from any host | `--target`, openkal, cross-compilation without editing the source |
3232
| 07 | [`examples/07-project-subos`](../examples/07-project-subos/) | A build program that finds its tools in the environment the project declared | `[xlings] subos`, `[xlings.workspace]`, a build program whose `PATH` is the environment the project named |
3333
| 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` |
34-
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in four programming models, with a CPU fallback in each | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
34+
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in several programming models, with a CPU fallback in each; plus one artifact carrying several backends at once | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
3535
| 09a | [`…/cuda`](../examples/09-heterogeneous/cuda/) | A CUDA kernel behind a seam module | `mcpp.rules.cuda`, `mcpp::action` with `role = "object"`, the driver stated as a fact and a floor |
3636
| 09b | [`…/vulkan`](../examples/09-heterogeneous/vulkan/) | The same computation as a Vulkan compute shader, on a GPU or on the CPU | `mcpp.rules.spirv`, `mcpp::action` with `role = "source"`, generated headers, a software driver as a payload |
3737
| 09c | [`…/sycl`](../examples/09-heterogeneous/sycl/) | The same computation as a SYCL kernel, compiled by a second compiler | `mcpp.rules.sycl`, the `.sycl` device extension, a chained `mcpp::action` for the device link, `compat:sycl-runtime` |
3838
| 09d | [`…/hip`](../examples/09-heterogeneous/hip/) | The same computation in HIP, reaching an NVIDIA device | `mcpp.rules.hip`, HIP as a header layer over the CUDA runtime, a two-chunk `accel` |
39+
| 09e | [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | Several backends in ONE artifact, chosen at run time — the library shape, not the program shape | `accel` as a set, `cfg(accelerator = "none")` and its negation, a dispatch chain, a module seam over a C island boundary |
40+
| 09f | [`…/cann`](../examples/09-heterogeneous/cann/) | An Ascend C kernel behind the same seam. **Does not build yet** — its README names the two missing pieces | the `.asc` device extension, `op_kernel`/`op_host` as an island CANN already has, `accelerator = "none"` for the fallback |
3941

4042
## Suggested Reading Order
4143

0 commit comments

Comments
 (0)