cuda: add the backend contract and its checker - #25
xiaoyu-xyz wants to merge 4 commits into
Conversation
6e25eee to
391cf45
Compare
Both models that need this readout share the shape of it: Kev (ThinkFlowLab#27) scores scale * dot(k_proj(c_i), q_proj(s)) and softmaxes over a question's candidates; CLM (ThinkFlowLab#9) scores exp(logit_scale) * cos(state_head(s), action_head(c)) and does the same. ThinkFlowLab#9 asks for exactly this fusion, and ThinkFlowLab#19's gemm.cu does not have it. The fusion is more than fewer launches. For a cosine the candidate's norm and its dot with the query need the same elements, so both accumulate in one read of C -- for Kev, 255 rows of 2560 floats read once instead of twice. The softmax then runs in-block over shared memory, so there is no second kernel, no atomics and no global round trip for the similarities. A batch is a grid over questions rather than a loop of launches. candidate_scoring.cu has NEVER BEEN COMPILED. The authoring machine has no CUDA toolkit and no NVIDIA GPU, so nothing here claims it compiles, runs or is fast. What is established is the arithmetic it performs: - reference.py is the float64 oracle, and its invariants are tested: a distribution, 1/K for indistinguishable candidates, no NaN for a zero candidate, no overflow where a naive softmax would produce inf. - kernel_simulation.py reproduces the kernel's algorithm in numpy -- float32 accumulation, the max-subtracted softmax, the norm floored as max(sqrt(sum), eps) and not sqrt(max(sum, eps)) -- and agrees with the oracle to a worst absolute difference of 1.8e-07 over the fixed cases. The simulation rounds twice per multiply-add where the kernel uses fmaf, so that is an upper bound on the kernel's error. - The reduction is a tree, matching __shfl_down_sync, tested structurally and shown to differ from a sequential sum on a concrete input. Tolerance is declared at 1e-4, three orders of magnitude above the simulated worst case, so a hardware failure is a failure rather than tolerance noise. gpu_parity.py checks a compiled library against the reference and skips with a reason when there is no GPU or no library. The inputs are not committed: they are deterministic from a fixed seed and would be about a megabyte of generated floats. 17 tests pass. The backend satisfies the contract from ThinkFlowLab#25, which caught the manifest over-claiming architectures the build script does not reach by default.
Both models that need this readout share the shape of it: Kev (ThinkFlowLab#27) scores scale * dot(k_proj(c_i), q_proj(s)) and softmaxes over a question's candidates; CLM (ThinkFlowLab#9) scores exp(logit_scale) * cos(state_head(s), action_head(c)) and does the same. ThinkFlowLab#9 asks for exactly this fusion and ThinkFlowLab#19's gemm.cu does not have it. The fusion is more than fewer launches. For a cosine the candidate's norm and its dot with the query need the same elements, so both accumulate in one read of C -- for Kev, 255 rows of 2560 floats read once instead of twice. The softmax runs in-block over shared memory, so there is no second kernel, no atomics and no global round trip for the similarities. A batch is a grid over questions. MEASURED, not just intended. RTX 4090 (sm_89), driver 595.58.03, CUDA 13.0 (V13.0.88): worst absolute difference: 1.835e-07 (tolerance 0.0001) all cases within tolerance All 13 fixed cases pass, including a single candidate, identical candidates, a zero candidate, K=255, and logits whose raw exponential overflows. Three defects were found and fixed; the first two only a GPU could show. 1. The parity harness passed host pointers where device pointers were expected, so the kernel dereferenced host memory as device memory. compute-sanitizer located it as an invalid global read with consecutive lanes on consecutive addresses. Fixed by allocating, copying and synchronising through cudart. 2. The query norm was summed across warps that had each computed the same partial: the inner loop already covers all of D within one warp, so the norm came out sqrt(8) too large on a 256-thread block. Every unnormalized case sat at float32 rounding while the normalize cases were off by ~1e-2, which is what pointed at it. Warp 0 now computes it alone. kernel_simulation.py did NOT catch this, because it was written from the intent rather than transcribed from the .cu. A simulation is only as good as its fidelity; hardware is what settles it. 3. The manifest claimed sm_89 while build.sh defaults to sm_80. The contract checker from ThinkFlowLab#25 caught that, as it caught the same class of over-claim in the Laya backend. The manifest is now status=validated with a declared tolerance and a reference entrypoint, which is what the contract requires of a backend making a parity claim. The reference is float64 in reference.py, and gpu_parity.py skips with a reason rather than passing when there is no GPU or no library. Not yet exercised: the batch path beyond questions=1, the K>255 rejection, and any architecture or CUDA version other than sm_89 on 13.0.
Both models that need this readout share the shape of it: Kev (ThinkFlowLab#27) scores scale * dot(k_proj(c_i), q_proj(s)) and softmaxes over a question's candidates; CLM (ThinkFlowLab#9) scores exp(logit_scale) * cos(state_head(s), action_head(c)) and does the same. ThinkFlowLab#9 asks for exactly this fusion and ThinkFlowLab#19's gemm.cu does not have it. The fusion is more than fewer launches. For a cosine the candidate's norm and its dot with the query need the same elements, so both accumulate in one read of C -- for Kev, 255 rows of 2560 floats read once instead of twice. The softmax runs in-block over shared memory, so there is no second kernel, no atomics and no global round trip for the similarities. A batch is a grid over questions. MEASURED, not just intended. RTX 4090 (sm_89), driver 595.58.03, CUDA 13.0 (V13.0.88): worst absolute difference: 1.835e-07 (tolerance 0.0001) all cases within tolerance All 13 fixed cases pass, including a single candidate, identical candidates, a zero candidate, K=255, and logits whose raw exponential overflows. Three defects were found and fixed; the first two only a GPU could show. 1. The parity harness passed host pointers where device pointers were expected, so the kernel dereferenced host memory as device memory. compute-sanitizer located it as an invalid global read with consecutive lanes on consecutive addresses. Fixed by allocating, copying and synchronising through cudart. 2. The query norm was summed across warps that had each computed the same partial: the inner loop already covers all of D within one warp, so the norm came out sqrt(8) too large on a 256-thread block. Every unnormalized case sat at float32 rounding while the normalize cases were off by ~1e-2, which is what pointed at it. Warp 0 now computes it alone. kernel_simulation.py did NOT catch this, because it was written from the intent rather than transcribed from the .cu. A simulation is only as good as its fidelity; hardware is what settles it. 3. The manifest claimed sm_89 while build.sh defaults to sm_80. The contract checker from ThinkFlowLab#25 caught that, as it caught the same class of over-claim in the Laya backend. The manifest is now status=validated with a declared tolerance and a reference entrypoint, which is what the contract requires of a backend making a parity claim. The reference is float64 in reference.py, and gpu_parity.py skips with a reason rather than passing when there is no GPU or no library. Not yet exercised: the batch path beyond questions=1, the K>255 rejection, and any architecture or CUDA version other than sm_89 on 13.0.
Both models that need this readout share the shape of it: Kev (ThinkFlowLab#27) scores scale * dot(k_proj(c_i), q_proj(s)) and softmaxes over a question's candidates; CLM (ThinkFlowLab#9) scores exp(logit_scale) * cos(state_head(s), action_head(c)) and does the same. ThinkFlowLab#9 asks for exactly this fusion and ThinkFlowLab#19's gemm.cu does not have it. The fusion is more than fewer launches. For a cosine the candidate's norm and its dot with the query need the same elements, so both accumulate in one read of C -- for Kev, 255 rows of 2560 floats read once instead of twice. The softmax runs in-block over shared memory, so there is no second kernel, no atomics and no global round trip for the similarities. A batch is a grid over questions. MEASURED, not just intended. RTX 4090 (sm_89), driver 595.58.03, CUDA 13.0 (V13.0.88): worst absolute difference: 1.835e-07 (tolerance 0.0001) all cases within tolerance All 13 fixed cases pass, including a single candidate, identical candidates, a zero candidate, K=255, and logits whose raw exponential overflows. Three defects were found and fixed; the first two only a GPU could show. 1. The parity harness passed host pointers where device pointers were expected, so the kernel dereferenced host memory as device memory. compute-sanitizer located it as an invalid global read with consecutive lanes on consecutive addresses. Fixed by allocating, copying and synchronising through cudart. 2. The query norm was summed across warps that had each computed the same partial: the inner loop already covers all of D within one warp, so the norm came out sqrt(8) too large on a 256-thread block. Every unnormalized case sat at float32 rounding while the normalize cases were off by ~1e-2, which is what pointed at it. Warp 0 now computes it alone. kernel_simulation.py did NOT catch this, because it was written from the intent rather than transcribed from the .cu. A simulation is only as good as its fidelity; hardware is what settles it. 3. The manifest claimed sm_89 while build.sh defaults to sm_80. The contract checker from ThinkFlowLab#25 caught that, as it caught the same class of over-claim in the Laya backend. The manifest is now status=validated with a declared tolerance and a reference entrypoint, which is what the contract requires of a backend making a parity claim. The reference is float64 in reference.py, and gpu_parity.py skips with a reason rather than passing when there is no GPU or no library. Not yet exercised: the batch path beyond questions=1, the K>255 rejection, and any architecture or CUDA version other than sm_89 on 13.0.
hsliuustc0106
left a comment
There was a problem hiding this comment.
Reviewed commit 391cf45e4013b833473e81b71508d547217f1605.
[P2] Include name in the missing-required-field guard. src/backends/cuda/check_contract.py:131 omits name from the early-return condition, then accesses manifest["name"]. A manifest missing only name produces KeyError instead of the intended structured validation report (including --json), aborting checks of other backends. Reproduced directly; all 53 existing tests pass.
19bdff7 to
a81a41a
Compare
|
Confirmed and fixed in The required-key list was written twice: once to report the keys missing, and again in the guard that returns before they are read. Both now derive from one |
Levius-Fubuki
left a comment
There was a problem hiding this comment.
Reviewed a81a41ae2ab8d1b15be868320fb2a85d97b283c7, including the checker, build-script parser, contract and tests.
The original missing-name finding is fixed: the old revision raises KeyError, while this head produces a structured missing-key issue. All 55 existing Python tests passed on Linux; the checker also ran successfully against this branch (which declares no backends).
Changes requested for three independently reproduced cases below. Each uses an isolated fixture and the real checker CLI; the valid control returns zero issues. No CUDA build or GPU parity claim is made.
|
resolve the comments please |
All three come from review on ThinkFlowLab#25. 1. reference.entrypoint was used as a path without checking its type. A list reached os.path.isabs, whose TypeError escaped check_manifest: the CLI printed no structured report at all, not even under --json, and every later backend went unchecked. Non-strings are now an Issue and checking continues. 2. A directory holding any *.backend.json was exempt from the undeclared-backend check, but discovery only loads <dir>/<dir>.backend.json. A manifest named typo.backend.json therefore bypassed every check for that backend: exit 0, checked: [], no errors and no warnings. A manifest that is present but not under the expected name is now reported. 3. Architectures read from `arch=` variables and targets written literally in a -gencode flag were combined with `or`, so the literals were discarded whenever a variable existed. A script assigning `arch=${2:-89}` and then compiling `-gencode arch=compute_90,code=sm_90` builds sm_90 only, yet a manifest declaring [89] passed with no findings. build_script.py now reports gencode_architectures: the targets that actually reach nvcc, resolved against the variable environment in force at each line, so a loop that reassigns `arch` still yields both targets. The checker treats those as the authority and reports an assigned value that never reaches a -gencode flag. 62 tests, seven of them new and one per defect. Verified against ThinkFlowLab#19's real build.sh, which still parses to variables [89] / gencode [89] and passes with no findings.
a81a41a to
dc3fd16
Compare
|
All three fixed in
Your third case now fails as 62 tests, one per defect plus the two guards. Verified against #19's real |
hsliuustc0106
left a comment
There was a problem hiding this comment.
Independent review — verdict: ready to approve. All findings are non-blocking. Everything below ran on CPU/macOS (Python 3.9.6); no CUDA involved.
Verified locally (detached worktree at dc3fd162):
python3 -m unittest discover -s src/backends/cuda/tests -t .— 62 tests, OKpython3 src/backends/cuda/check_contract.py --repo-root .— exit 0,no backends declared✓ (true at this PR's base, which predatesqwen3_5/; see finding 3)- The build-script fixture matches the real
qwen3_5/build.shargument handling verbatim (out=${1:?…},arch=${2:-${CUDA_COMPUTE_CAP:-89}},-gencode "arch=compute_${arch},…",-o "$out/libqwen3_5_cuda.so"), and the parser reads scripts as text without executing them ✓ - The
contract.mdexample manifest, written asqwen3_5/qwen3_5.backend.jsonagainst the realqwen3_5/tree: accepted, 0 errors, 2 warnings (src/models/kev/andrecipe/cua_s1/check_native.pydon't exist yet) — both by design
Findings (non-blocking):
- The example manifest understates the real ABI.
contract.mdshows"abi_version": 1for a manifest presented as qwen3_5's shape (its exact 9 sources,status: validated), but the real library isCS1_ABI_VERSION 3(src/backends/cuda/qwen3_5/ops.h:16). The checker can't catch that mismatch (it doesn't read headers), so the example is the only guard — please use 3, or state the numbering explicitly. - Wrong file cited for the sm_80 note. §3 says "
#19'sattention.cudocuments sm_80+" —attention.cuhas no arch note at main. The notes live inmma.cuh:1andgdn_prefill.cu:1. - "No backends declared" holds only at this PR's base. Merged into current main, the checker exits 1:
[error] qwen3_5: has kernel sources (...) but no qwen3_5.backend.json manifest— verified in a simulated merge. That is the forcing function working as designed, but it makes aqwen3_5.backend.jsonfollow-up required, not optional; worth stating here or landing the manifest in this PR. - Body numbers are stale at the head commit: 7 files / +1630 (not 6 / +1448), 62 tests (not 53), 644 core lines (not 566) — so the overage vs the 500-line guideline is ~29%, not "slightly over".
- Nits: the three
ParseBuildScriptTestcases are duplicated betweentest_build_script.pyandtest_check_contract.py; the "not executable in git" message checks the filesystem bit (os.access), not the index — wording only, and the suggested fix (git update-index --chmod=+x) is the right one.
Not verified here: the tampering matrix and the #16 tools/ rejection (no tools/ on main to point the checker at), and CI coverage — ci.yml runs only the Rust suite and tests/benchmarks, so neither these tests nor the checker run in CI. The PR states that wiring is separate and it stays out of scope; agreed.
All from the independent review on ThinkFlowLab#25. The manifest's `abi_version` was ambiguous and unverifiable. contract.md described a per-library `<prefix>_abi_version()` symbol but never said the manifest field was that number, and its example showed 1 for a manifest presented as qwen3_5's shape — while ThinkFlowLab#19's ops.h says CS1_ABI_VERSION 4. Nothing read the header, so a manifest disagreeing with its own library was accepted and the example was the only guard. `abi_version` is now defined as the library's own, and the checker reads the `#define <PREFIX>_ABI_VERSION N` out of the declared sources and requires the manifest to match. Against the real tree this reports abi_version is 1 but ops.h defines CS1_ABI_VERSION 4 and passes at 4. A backend whose sources define no such macro gets a warning rather than silence, and sources defining two different values are an error. The cross-backend sameness rule is removed. It would have forced unrelated model engines onto one interface version, which the repository layout explicitly does not ask for; each manifest now has to match its own header instead. A test asserts two backends may declare different versions. Two documentation errors the review found: - §3 cited `attention.cu` for the sm_80 note. That file has none; `mma.cuh` and `gdn_prefill.cu` each say "sm_80 and later" on their first line. - The example manifest now uses 4, matching ops.h. Two nits: three parser cases were duplicated between test_build_script.py and test_check_contract.py and are now only in the former, where the parser is tested; and the not-executable message said "in git" while it checks the working tree's bit, which a fresh clone takes from the committed one. 63 tests.
The manifest said abi_version 1. ThinkFlowLab#19's ops.h defines CS1_ABI_VERSION 4, so the declaration disagreed with the library it describes. Nothing caught it when this was written, because the checker did not read headers. ThinkFlowLab#25 now does: it reads the `#define <PREFIX>_ABI_VERSION N` out of the declared sources and requires the manifest to match, which is what surfaced this. abi_version is 1 but ops.h defines CS1_ABI_VERSION 4 also strengthens the description in the same PR.
|
All five addressed in
The header gap was the useful one — thanks. A convention whose only enforcement is an example in a document is not enforced. |
…ed against `cs_score_abi_version()` returned a literal and nothing else in the source stated the number, so `scoring.backend.json`'s `abi_version` had no counterpart to disagree with. ThinkFlowLab#25 now reads `#define <PREFIX>_ABI_VERSION N` out of a backend's declared sources and requires the manifest to match; this backend was the one it reported a warning for. The macro is defined once and the function returns it, so the two cannot drift: the manifest says 1, the macro says 1, and a manifest saying anything else is now reported as `abi_version is 2 but candidate_scoring.cu defines CS_SCORE_ABI_VERSION 1`. No behaviour change: the function body expands to `return 1;` exactly as before, so the compiled library is unchanged apart from line numbers. The file has not been recompiled here -- there is no CUDA toolkit on this machine -- but the preprocessed result is what it was. 17 tests still pass.
|
Rechecked |
hsliuustc0106
left a comment
There was a problem hiding this comment.
Independent local review — against the current architecture baseline
Verdict: changes requested — two Medium findings; the mechanism itself is excellent and verified.
- [Medium]
contract.mdrestates the superseded ownership boundary. Its intro ("model orchestration, batching policy, state management and kernel selection stay with the model engine") is the pre-#76 rule and now contradicts bothsrc/backends/cuda/README.mdin the same directory anddocs/architecture.md: the shared worker runtime owns processing orchestration, batching policy, and request bookkeeping (serial admission implemented in #80), while model executors own forward passes, device state, and kernel selection. Since #77's author will read this document as the guide, please replace the restatement with a link to the architecture contracts and update the "model engines" vocabulary. - [Medium] Nothing runs the tests or the checker. The only Python discovery in
ci.ymlistests/benchmarks. Please land a CI step (unittest discover forsrc/backends/cuda/tests+check_contract.py --repo-root .) together with #77 — I confirmed the checker exits 1 on current main until #77's manifest lands, so wiring it first would redden main. - [Low]
check_contract.pydocstring says "Tier 2 (--gpuin CI)" — no such flag exists, andcontract.mddefines Tier 2 = compile, Tier 3 = GPU parity.CONTRACT_ABI_VERSION = 1is really a minimum library ABI; mildly misleading next to "abi_version is not a version of this document". - [Low] The multi-model example and rationale cite hypothetical Kev (#9); the tree's real second consumer is Open-Jev via the shared
qwen3_5executor.
Independently verified (head 19f95a4, tarball): 63/63 tests pass (system Python, zero deps); checker behavior on three trees — PR tree exit 0, current main exit 1 with exactly the disclosed "qwen3_5 has kernel sources but no manifest" error, and current main + #77's manifest overlaid → 0 errors, 0 warnings with the new ABI-macro check genuinely cross-checking ops.h's CS1_ABI_VERSION 4 against abi_version: 4. Parser never executes scripts; sm_80 claims in mma.cuh/gdn_prefill.cu verified verbatim. The manifest/ABI checker operationalizes exactly what docs/architecture.md mandates ("keep Rust declarations and the native ABI in sync") — the tier honesty also matches repo policy.
Local reviewer report per the repo review skill; this review reflects head 19f95a4 only.
…cture From review on ThinkFlowLab#25. **The crash.** `_declared_abi` iterated `manifest.get("sources") or []` without checking the container, so `"sources": 42` or `true` raised TypeError out of check_manifest. That loses the whole report, including --json, and every later backend -- the same failure the string check on `reference.entrypoint` was added to prevent. It was added one commit earlier and I reintroduced the shape immediately, so the fix is a test over the class rather than another one-off: for every required field and every `build` subfield, and for the optional ones, a spread of wrong types must produce a report and not an exception. That test fails against `19f95a4` and passes here. **Ownership.** The introduction restated the pre-ThinkFlowLab#76 rule, which now contradicts `src/backends/cuda/README.md` and `docs/architecture.md` in the same tree. The shared worker runtime owns processing orchestration, batching policy and request bookkeeping; model executors own forward passes, device state and kernel selection. The document now links to the architecture contracts instead of restating them, since a restatement is what drifted, and the vocabulary is "model executor" throughout. **The second consumer is real now.** The rationale and the `models` example cited a hypothetical Kev engine; the tree has Open-Jev alongside Cua-S1, both running through `src/models/qwen3_5/native/`. The example names those. **Docstring.** It described "Tier 2 (`--gpu` in CI)", which is neither a flag this script has nor the tier numbering contract.md uses. Tier 2 compiles, Tier 3 runs parity, and neither is reached from here. `CONTRACT_ABI_VERSION` is renamed `MINIMUM_ABI_VERSION`, because it is a floor on a library's own version rather than a version of the document. 66 tests.
Nothing ran `src/backends/cuda/tests` or `check_contract.py`: the only Python discovery in this workflow is `tests/benchmarks`. Two things make this land here rather than with the checker. The step needs `check_contract.py`, which arrives in ThinkFlowLab#25. And the checker exits 1 on a tree that has kernels but no declaration for them, which is main's state until the manifest in this branch lands — so wiring it up first would redden main. Both are stated in the job's comment and in its failure message, so a wrong merge order is self-explanatory rather than a bare "file not found". No CUDA toolkit and no GPU: the checker and its tests read files as text.
|
All addressed in The crash (@Levius-Fubuki). Correct, and it is the same shape as the one [Medium] Ownership. You are right that it restated the pre-#76 rule and contradicted [Medium] CI. Landed in #77 rather than here, deliberately: the step needs [Low] Docstring. Fixed: it described a [Low] The second consumer. Fixed. The rationale and the 66 tests. The ABI check is unchanged and still verifies |
The job failed on its own branch, in five seconds:
ImportError: Start directory is not importable: 'src/backends/cuda/tests'
The checker and its tests live in ThinkFlowLab#25, so on ThinkFlowLab#77's tree there is nothing to run.
The guard I wrote checked for `check_contract.py` alone and ran *after* the test
step, so the first step failed before the guard was reached and the message
explaining the dependency never printed.
Now a first step decides whether the prerequisites are present and the three
steps run only if they are; otherwise the job reports a notice naming ThinkFlowLab#25 and
passes. That is what makes either merge order work: green on this branch before
ThinkFlowLab#25 lands, and running by itself once it does.
Verified both ways: with ThinkFlowLab#25's files absent all three steps skip, and with them
present 66 checker tests, 66 kernel tests and the contract check all pass.
|
Rechecked |
ThinkFlowLab#19 added the kernels under src/backends/cuda/qwen3_5/ but left them undeclared, so the directory has kernel sources and no manifest. ThinkFlowLab#25 defines the manifest and its checker, and running that checker against main reports exactly that: [error] qwen3_5: has kernel sources (attention.cu, common.cuh, elementwise.cu) but no qwen3_5.backend.json manifest This adds the declaration, with the values the merged files already state: - sources: the nine kernel and header files, from the directory listing. - build.script and build.output: what build.sh actually writes, libqwen3_5_cuda.so. - build.default_arch 89 and build.architectures [89]: build.sh defaults `arch` to CUDA_COMPUTE_CAP-or-89 and passes it to a single -gencode flag. - build.min_capability 80: the README says "Tensor-core kernels need sm_80 or newer". status is `experimental`, not `validated`. ThinkFlowLab#19 validated the kernels, but this manifest is written by someone other than their author and a `validated` manifest asserts a tolerance and a reference entrypoint, which are twu3202's to state. A declaration that the backend exists and how it builds is useful on its own and does not put words in anyone's mouth; promoting it is a one-line change once the tolerance is recorded. Checked with the checker from ThinkFlowLab#25: `1 backend(s), 0 error(s), 0 warning(s)`, and with discover_buildable.py from #2, which reports it as compile-checkable.
The manifest said abi_version 1. ThinkFlowLab#19's ops.h defines CS1_ABI_VERSION 4, so the declaration disagreed with the library it describes. Nothing caught it when this was written, because the checker did not read headers. ThinkFlowLab#25 now does: it reads the `#define <PREFIX>_ABI_VERSION N` out of the declared sources and requires the manifest to match, which is what surfaced this. abi_version is 1 but ops.h defines CS1_ABI_VERSION 4 also strengthens the description in the same PR.
Nothing ran `src/backends/cuda/tests` or `check_contract.py`: the only Python discovery in this workflow is `tests/benchmarks`. Two things make this land here rather than with the checker. The step needs `check_contract.py`, which arrives in ThinkFlowLab#25. And the checker exits 1 on a tree that has kernels but no declaration for them, which is main's state until the manifest in this branch lands — so wiring it up first would redden main. Both are stated in the job's comment and in its failure message, so a wrong merge order is self-explanatory rather than a bare "file not found". No CUDA toolkit and no GPU: the checker and its tests read files as text.
The job failed on its own branch, in five seconds:
ImportError: Start directory is not importable: 'src/backends/cuda/tests'
The checker and its tests live in ThinkFlowLab#25, so on ThinkFlowLab#77's tree there is nothing to run.
The guard I wrote checked for `check_contract.py` alone and ran *after* the test
step, so the first step failed before the guard was reached and the message
explaining the dependency never printed.
Now a first step decides whether the prerequisites are present and the three
steps run only if they are; otherwise the job reports a notice naming ThinkFlowLab#25 and
passes. That is what makes either merge order work: green on this branch before
ThinkFlowLab#25 lands, and running by itself once it does.
Verified both ways: with ThinkFlowLab#25's files absent all three steps skip, and with them
present 66 checker tests, 66 kernel tests and the contract check all pass.
`declared_roots` held the backend directories discovery found. For a flat backend -- one whose manifest sits directly in src/backends/cuda/, as `laya.backend.json` does -- that directory is the cuda directory itself, so the undeclared-backend sweep skipped every subdirectory beneath it. Once any flat backend existed the check could no longer fire: on current main `qwen3_5/` holds kernels with no manifest and the checker still exits 0. Ownership now follows what the manifest states. A `<name>/` backend owns its own directory, and a flat backend owns the subdirectories its `sources` name, so Laya still covers `kernels/` and `tools/` while `qwen3_5/` beside it is reported. Also corrects three descriptions that no longer matched the code: the module docstring's discovery path and its removed cross-backend ABI-consistency rule, the compute-capability bullet, and the same consistency claim in contract.md.
Purpose
CUDA build paths were diverging: Laya generates CUDA from TileLang (#14), Cua-S1 hand-writes CUDA C++ with cuBLASLt (#19), and the multimodal worker plans Triton (#12). Nothing linked against anything else, so the divergence was invisible until one model reused another's kernels.
This adds the contract and the checker:
contract.md— what backends must agree on: a JSON manifest per backend, four C ABI rules, the numerics that must be declared rather than discovered in a parity failure, and the build-script interface.check_contract.py— reads the manifests and checks them against that contract.build_script.py— reads a build script as text and reports what it declares. Never executed: CI must not run repository code to decide whether a manifest is honest.A backend may be a subdirectory named after itself or sit directly under
src/backends/cuda/, which is the layout Laya'skernels/andtools/use.What the checker enforces
build.shdeclares the architectures the manifest claims and writes the library the manifest names — from the-gencodeflags, resolved against the variable environment in force at each line, so a variable that never reaches nvcc cannot make a target look reachable.abi_versionmatches the<PREFIX>_ABI_VERSIONmacro in the declared sources. This is the one field whose truth lives in the code, and nothing read it before.validatedbackend declares both a tolerance and a reference entrypoint.Test Plan
Test Result
68 tests. Verified on a clean
git archiveof the head commit, not only in a working tree.Against real trees rather than fixtures: the merged
qwen3_5/is accepted at its realabi_version4 and reports a mismatch when the manifest says otherwise; a manifest namedtypo.backend.jsonis reported instead of silently exempting the directory; a non-stringreference.entrypointproduces a structured issue rather than aTypeErrorthat suppressed the whole report; andqwen3_5/beside Laya's flat manifest is reported as undeclared, which it was not while a flat backend was taken to own the wholecuda/directory.On the 500-line guideline
741 lines of core Python (
check_contract.py514,build_script.py227) — about 48% over.contract.mdis documentation and not counted. The two files are one change: the parser is what the checker reads build scripts with, and separating them would ship a checker that cannot run. Splitting further would mean shipping a checker that does not check what this one does.Status against current
mainmainhasqwen3_5/from #19 and no manifest for it, so the checker exits 1 there. That is the forcing function working, and it makes the manifest a required follow-up rather than an optional one — it is #77, which also correctsabi_versionto the library's real 4. Laya's own flat manifest passes with one warning: none of its declared sources defines anABI_VERSIONmacro, so itsabi_version: 1cannot be checked against the library.Deliberately not included
contract.mdstates them as requirements and wiring them is separate, so this needs no CUDA toolkit and no GPU.build.architectures, which would needcuobjdump.Related: #6 (layout), #9 (model roadmap), #12, #14, #19.