Skip to content

cuda: add the backend contract and its checker - #25

Open
xiaoyu-xyz wants to merge 4 commits into
ThinkFlowLab:mainfrom
xiaoyu-xyz:cuda-backend-contract
Open

xiaoyu-xyz wants to merge 4 commits into
ThinkFlowLab:mainfrom
xiaoyu-xyz:cuda-backend-contract

Conversation

@xiaoyu-xyz

@xiaoyu-xyz xiaoyu-xyz commented Sep 28, 2026 •

Copy link
Copy Markdown

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's kernels/ and tools/ use.

What the checker enforces

  • The manifest schema, and that every declared source exists and stays inside the backend directory.
  • That a directory holding kernels with no manifest is reported — including one sitting beside a backend declared in the parent directory.
  • That build.sh declares the architectures the manifest claims and writes the library the manifest names — from the -gencode flags, resolved against the variable environment in force at each line, so a variable that never reaches nvcc cannot make a target look reachable.
  • That abi_version matches the <PREFIX>_ABI_VERSION macro in the declared sources. This is the one field whose truth lives in the code, and nothing read it before.
  • That a validated backend declares both a tolerance and a reference entrypoint.

Test Plan

python3 -m unittest discover -s src/backends/cuda/tests -t .
python3 src/backends/cuda/check_contract.py --repo-root .

Test Result

68 tests. Verified on a clean git archive of the head commit, not only in a working tree.

Against real trees rather than fixtures: the merged qwen3_5/ is accepted at its real abi_version 4 and reports a mismatch when the manifest says otherwise; a manifest named typo.backend.json is reported instead of silently exempting the directory; a non-string reference.entrypoint produces a structured issue rather than a TypeError that suppressed the whole report; and qwen3_5/ beside Laya's flat manifest is reported as undeclared, which it was not while a flat backend was taken to own the whole cuda/ directory.

On the 500-line guideline

741 lines of core Python (check_contract.py 514, build_script.py 227) — about 48% over. contract.md is 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 main

main has qwen3_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 corrects abi_version to the library's real 4. Laya's own flat manifest passes with one warning: none of its declared sources defines an ABI_VERSION macro, so its abi_version: 1 cannot be checked against the library.

Deliberately not included

  • Compile and parity tiers. They need a CUDA container and a GPU runner; contract.md states them as requirements and wiring them is separate, so this needs no CUDA toolkit and no GPU.
  • Extracting a shared CUDA runtime surface. One backend exists, so the shape would be guesswork.
  • Reading the built artifact. A build check proves the sources compile and a file of the declared name appears; it does not prove the binary honoured build.architectures, which would need cuobjdump.

Related: #6 (layout), #9 (model roadmap), #12, #14, #19.

@xiaoyu-xyz
xiaoyu-xyz force-pushed the cuda-backend-contract branch 3 times, most recently from 6e25eee to 391cf45 Compare September 28, 2026 09:36
@xiaoyu-xyz xiaoyu-xyz changed the title cuda: add the backend contract, a contract check, and CUDA CI cuda: add the backend contract and its checker Sep 28, 2026
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Sep 28, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Sep 28, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Sep 28, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Sep 28, 2026
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 hsliuustc0106 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@xiaoyu-xyz
xiaoyu-xyz force-pushed the cuda-backend-contract branch 2 times, most recently from 19bdff7 to a81a41a Compare September 30, 2026 01:12
@xiaoyu-xyz

Copy link
Copy Markdown
Author

Confirmed and fixed in a81a41a — thanks for the precise mechanism.

The required-key list was written twice: once to report the keys missing, and again in the guard that returns before they are read. name was in the first only. A manifest missing just name therefore reported it and then read it anyway, raising out of check_manifest — which also dropped the other backends' results and the --json output, as you saw.

Both now derive from one REQUIRED_KEYS constant. The tests remove each required key in turn and assert a structured error rather than an exception, plus one that reproduces your case directly: a bad manifest beside a good one, asserting the good one is still checked and listed.

@Levius-Fubuki Levius-Fubuki left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/backends/cuda/check_contract.py Outdated
Comment thread src/backends/cuda/check_contract.py Outdated
Comment thread src/backends/cuda/check_contract.py Outdated
@hsliuustc0106

Copy link
Copy Markdown
Contributor

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.
@xiaoyu-xyz
xiaoyu-xyz force-pushed the cuda-backend-contract branch from a81a41a to dc3fd16 Compare October 3, 2026 11:57
@xiaoyu-xyz

Copy link
Copy Markdown
Author

All three fixed in dc3fd16.

  1. reference.entrypoint is type-checked before it is used as a path. A non-string is an Issue and the remaining backends are still checked, so --json always produces a report.
  2. A manifest that is present but not under the name discovery looks for is now reported rather than exempting the directory — has typo.backend.json but not x.backend.json, so it is never discovered.
  3. build_script.py now reports the targets that actually reach nvcc, taken from the -gencode flags and 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 flag.

Your third case now fails as build script assigns [89] but only passes [90] to nvcc.

62 tests, one per defect plus the two guards. Verified against #19's real build.sh, which still parses to variables [89] / gencode [89] and passes with no findings.

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, OK
  • python3 src/backends/cuda/check_contract.py --repo-root . — exit 0, no backends declared ✓ (true at this PR's base, which predates qwen3_5/; see finding 3)
  • The build-script fixture matches the real qwen3_5/build.sh argument 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.md example manifest, written as qwen3_5/qwen3_5.backend.json against the real qwen3_5/ tree: accepted, 0 errors, 2 warnings (src/models/kev/ and recipe/cua_s1/check_native.py don't exist yet) — both by design

Findings (non-blocking):

  1. The example manifest understates the real ABI. contract.md shows "abi_version": 1 for a manifest presented as qwen3_5's shape (its exact 9 sources, status: validated), but the real library is CS1_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.
  2. Wrong file cited for the sm_80 note. §3 says "#19's attention.cu documents sm_80+" — attention.cu has no arch note at main. The notes live in mma.cuh:1 and gdn_prefill.cu:1.
  3. "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 a qwen3_5.backend.json follow-up required, not optional; worth stating here or landing the manifest in this PR.
  4. 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".
  5. Nits: the three ParseBuildScriptTest cases are duplicated between test_build_script.py and test_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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 4, 2026
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.
@xiaoyu-xyz

Copy link
Copy Markdown
Author

All five addressed in 19f95a4.

  1. abi_version. You were right that the example was the only guard, and that nothing read the header. The field is now defined as the library's own rather than a version of the document, and the checker reads #define <PREFIX>_ABI_VERSION N out of the declared sources and requires the manifest to match. Against the real tree it reports abi_version is 1 but ops.h defines CS1_ABI_VERSION 4; the value is 4, not 3 — ops.h has been bumped since you read it. No macro in the sources is a warning rather than silence, two different values inside one backend is an error, and the cross-backend sameness rule is gone: it would have forced unrelated engines onto one interface version, which the layout explicitly does not ask for. A test asserts two backends may declare different versions.
  2. Fixed — mma.cuh and gdn_prefill.cu, both of which say "sm_80 and later" on line 1.
  3. Agreed, and cuda: declare the qwen3_5 backend #77 is that follow-up. It also corrects the manifest's abi_version to 4, which your finding 1 is what surfaced. The body now says it is required rather than optional.
  4. Body corrected and stated plainly: 6 files / +1719, 63 tests, 696 lines of core Python, 39% over the guideline.
  5. The three parser cases now live only in test_build_script.py, where the parser is tested; and the message no longer says "in git", since it reads the working tree's execute bit.

The header gap was the useful one — thanks. A convention whose only enforcement is an example in a document is not enforced.

xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 4, 2026
…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.
@Levius-Fubuki

Copy link
Copy Markdown
Collaborator

Rechecked 19f95a4: the original fixes pass, but the new ABI scan crashes on "sources": 42 or true, aborting the entire check without JSON output. Please validate sources before scanning and add a malformed-manifest regression test.

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent local review — against the current architecture baseline

Verdict: changes requested — two Medium findings; the mechanism itself is excellent and verified.

  1. [Medium] contract.md restates 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 both src/backends/cuda/README.md in the same directory and docs/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.
  2. [Medium] Nothing runs the tests or the checker. The only Python discovery in ci.yml is tests/benchmarks. Please land a CI step (unittest discover for src/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.
  3. [Low] check_contract.py docstring says "Tier 2 (--gpu in CI)" — no such flag exists, and contract.md defines Tier 2 = compile, Tier 3 = GPU parity. CONTRACT_ABI_VERSION = 1 is really a minimum library ABI; mildly misleading next to "abi_version is not a version of this document".
  4. [Low] The multi-model example and rationale cite hypothetical Kev (#9); the tree's real second consumer is Open-Jev via the shared qwen3_5 executor.

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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.
@xiaoyu-xyz

Copy link
Copy Markdown
Author

All addressed in 6b3f0e6, with the CI step in #77.

The crash (@Levius-Fubuki). Correct, and it is the same shape as the one reference.entrypoint was fixed for — _declared_abi iterated manifest.get("sources") or [] without checking the container, so 42 or true raised out of check_manifest and cost the whole report. I reintroduced it one commit after fixing its twin, so the fix is a test over the class rather than another one-off: every required field, every build subfield, and the optional ones, each given a spread of wrong types, must produce a report and not an exception. That test fails against 19f95a4.

[Medium] Ownership. You are right that it restated the pre-#76 rule and contradicted src/backends/cuda/README.md and docs/architecture.md in the same tree. The introduction now links to the architecture contracts instead of restating them — a restatement is what drifted — and the vocabulary is "model executor" throughout.

[Medium] CI. Landed in #77 rather than here, deliberately: the step needs check_contract.py, and the checker exits 1 on trees that have kernels and no declaration, so it cannot land before the manifest either. Both are stated in the job comment and in its failure message so a wrong merge order explains itself. Verified by simulating the two steps against #25@6b3f0e6 + #77@67c7b65: 66 tests, check_contract exit 0.

[Low] Docstring. Fixed: it described a --gpu flag this script does not have and a tier numbering contract.md does not use. Tier 2 compiles, Tier 3 runs parity, neither is reachable from here. CONTRACT_ABI_VERSION is now MINIMUM_ABI_VERSION, since it is a floor on a library's own version rather than a version of the document.

[Low] The second consumer. Fixed. The rationale and the models example cited a hypothetical Kev engine; they now name src/models/cua_s1/native/ and src/models/open_jev/native/, both of which run through src/models/qwen3_5/native/.

66 tests. The ABI check is unchanged and still verifies CS1_ABI_VERSION 4 against the manifest on the real tree.

xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.
@Levius-Fubuki

Copy link
Copy Markdown
Collaborator

Rechecked 6b3f0e6: sources: 42 and true now produce structured schema errors without aborting the report. All 66 tests and the previous defect probes pass. No new functional findings in this follow-up.

xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.
xiaoyu-xyz added a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants