Skip to content

unsloth: prove the merged tree still has the features, and that it compiles - #175

Merged
danielhanchen merged 2 commits into
masterfrom
pin-contract/verify-the-merged-tree
Sep 4, 2026
Merged

unsloth: prove the merged tree still has the features, and that it compiles#175
danielhanchen merged 2 commits into
masterfrom
pin-contract/verify-the-merged-tree

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

The nightly proves the pins merged. It has never proved they are in the release, and that gap has cost three outages:

what happened
ggml-org#28133 upstream squashed it, so the pinned commit stopped being an ancestor and the merge re-applied work the base already had. It happened to conflict, which is the only reason anybody noticed. Merging quietly would have shipped nothing.
ggml-org#27754 merged with zero conflicts and did not compile. Upstream had added n_kv_max to build_attn_mha; the pin's new build_attn_sparse still called the old signature. The release died in the CUDA leg after the 38-job fan-out.
08-27 a resolution that kept the wrong side and compiled fine. merge_checks.py exists because of this one.

merge_checks.py asserts the absence of two known-bad shapes. Nothing anywhere asserts the presence of anything, and nothing compiles the merged tree before thirty-eight build jobs start.

scripts/unsloth/pin_contract.py

Derives from each pin's own diff what it puts in the tree, then checks the merged tree still has it. Nothing to maintain: the expectation comes out of the commit, so a repin regenerates it.

  • symbols -- every LLM_ARCH_ / GGML_OP_ / PROJECTOR_TYPE_ / GGML_TYPE_ / LLAMA_FTYPE_ / LLM_KV_ / LLM_TENSOR_ / LLM_TYPE_ name the pin introduces, per file, not per tree. That distinction is the whole point: LLM_ARCH_INKLING surviving in llama-arch.h while its dispatch arm is gone from llama-model.cpp is exactly the failure being looked for, and a tree-wide grep passes it.
  • files -- every file the pin adds still exists.
  • lines -- every non-comment code line the pin adds is still in that file. Catches a hunk eaten without touching a symbol.
  • redundancy -- reported, never fatal.

Comments are stripped before anything is read off a line. unslothai#70 has a comment naming GGML_OP_SSM_SCAN to explain why it does not use it; holding comment wording as a contract fails the moment upstream rewords it.

On redundancy

A pin whose added lines the base tag already has is work upstream has taken. That is the retirement rule already written in pr-set.json's _doc, mechanised, and it has to compare text rather than ancestry because upstream almost always squashes, so no ancestry test will ever say the work landed.

Measured on the real set at b10775, the separation is not close:

   28133 (retired):   318/  320 of its added lines are ALREADY in b10775  (99%)
           70 Kimi:    45/  133                                          (33%)
           144 MTP:    61/  346                                          (17%)
       172 Inkling:   238/ 2978                                           (7%)
     107 DiffGemma:   112/ 2084                                           (5%)
      157 tiny fix:     0/    3                                           (0%)

Threshold 0.95. Not fatal, deliberately: upstream landing a feature overnight must not stop that night's release.

The compile gate

CPU only, llama target only. That is where src/llama-graph.cpp lives, which is the translation unit that failed on 09-03. Measured on the merged b10775 tree, cold, ccache disabled, at -j4 to match a hosted runner's core count:

configure     1.07 s
COLD BUILD   59.45 s

Against twenty to sixty minutes for a CUDA build. It runs in resolve before the fan-out, and again in the preflight seven hours earlier.

Workflow plumbing

resolve now copies all of scripts/unsloth/ to $RUNNER_TEMP/us in one line instead of one cp per script. That block is within a few hundred characters of GitHub's 21000-character per-string cap, where going over silently disables the entire workflow -- no jobs, no annotations, no error. This leaves it 121 characters smaller than before while making every future check free:

before: 20615 of 21000
after:  20494 of 21000

The pin list reaches the checker through env:, never ${{ }} inline. It carries PR titles, which are third-party text, and ${{ }} pastes them into the shell source before bash sees it.

The preflight gets both checks appended to the PROBLEMS accumulator that already feeds prebuilt-alert, so findings land in the run summary with no new plumbing.

Testing

scripts/unsloth/test_pin_contract.py, added to the lint workflow's resolver-tests loop. Seventeen checks, each building a real repository with a real base tag, a real pin branch and a real merge, then damaging the merged tree the way a bad resolution damages it.

Beyond the unit tests, three regressions reproduced against the real b10775 mix:

A retired pin is spotted. Running against master's pin set, which still lists ggml-org#28133:

note ggml-org#28133: the base tag already has 318/320 (99%) of the lines this pin adds;
     upstream has taken this work and the entry should be deleted from pr-set.json

A dropped dispatch arm is caught. Deleting the PROJECTOR_TYPE_KIMIK3 builder arm from tools/mtmd/clip.cpp:

FAIL unslothai#70
     tools/mtmd/clip.cpp kept 20/21 of the lines this pin adds (95%);
     first missing: builder = std::make_unique<clip_graph_kimik3>(ctx, img);

Note that one was caught by line survival, not by the symbol check, because PROJECTOR_TYPE_KIMIK3 still appears elsewhere in the same file. That is why both checks exist.

The 09-03 failure is caught in a minute. Reverting the n_kv_max fix in the merged tree:

src/llama-graph.cpp:3715:39: error: no matching function for call to
  'llm_graph_context::build_attn_mha(..., ggml_tensor*&, float&, int&) const'

And on the intact tree, all twelve pins pass:

all 12 pins are intact in the merged tree

All six resolver test suites pass, and check_workflow_scalars.py is clean.

What this does not do

The contract is re-derived from the pin, so it only ever proves the merge lost nothing. A regression inside the pin itself regenerates a smaller contract that passes. Proving a feature actually runs needs a build and a test, and that is the next change: a feature-keyed manifest plus test-llama-archs / test-backend-ops probes. Two of the features we ship, DiffusionGemma and Inkling, have no runtime test at all today.

…mpiles

The nightly proves the pins merged. It has never proved they are in the
release, and the gap has cost three outages:

  ggml-org#28133   upstream squashed it, so the pinned commit stopped being an
                   ancestor and the merge re-applied work the base already had.
                   It happened to conflict, which is the only reason anybody
                   noticed. Merging quietly would have shipped nothing.
  ggml-org#27754   merged with zero conflicts and did not compile. Upstream had
                   added n_kv_max to build_attn_mha; the pin's new
                   build_attn_sparse still called the old signature. The
                   release died in the CUDA leg after the 38-job fan-out.
  08-27            a resolution that kept the wrong side and compiled fine.

Three additions, cheapest first.

pin_contract.py derives from each pin's own diff what it puts in the tree,
then checks the merged tree still has it: every LLM_ARCH_/GGML_OP_/
PROJECTOR_TYPE_/... name, per FILE and not per tree, because the failure being
looked for is exactly LLM_ARCH_INKLING surviving in llama-arch.h while its
dispatch arm is gone; every file the pin adds; and every non-comment code line
it adds, which catches a hunk eaten without touching a symbol. Nothing to
maintain: the expectation comes out of the commit, so a repin regenerates it.

It also reports, never fatally, a pin whose added lines the base tag already
has. That is pr-set.json's retirement rule mechanised, and it needs to be text
rather than ancestry because upstream squashes. The separation is not close:
measured at b10775 the pin upstream had absorbed scored 99% and the highest
live pin scored 33%.

Not fatal, deliberately: upstream landing a feature overnight must not stop
that night's release.

A CPU compile gate in resolve, before the fan-out. `llama` target only, which
is where llama-graph.cpp lives, and CPU only: 59 seconds cold at -j4 with no
ccache, against 20 to 60 minutes for CUDA.

The preflight gets both, seven hours ahead of the nightly on a throwaway
runner, appending to the PROBLEMS accumulator that already feeds the alert.

resolve now copies all of scripts/unsloth/ to $RUNNER_TEMP in one line instead
of one cp per script. That block is within a few hundred characters of the
21000 cap, where going over silently disables the entire workflow, and this
leaves it 121 characters smaller than before while making every future check
free. The pin list reaches the checker through env, not ${{ }}: it carries PR
titles, which are third-party text.

What this cannot do, so nobody reads more into a pass than is there: the
contract is re-derived from the pin, so it only proves the MERGE lost nothing.
A regression inside the pin regenerates a smaller contract that passes, and
proving a feature runs needs a build and a test.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T11:31:51.429413Z c21aa43 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7828fbc0cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +176 to +177
if ! python3 ../scripts/unsloth/pin_contract.py --root . --base "$BASE" \
--pr-set ../scripts/unsloth/pr-set.json --report "${RUNNER_TEMP}/pin_contract.json" ; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check only pins the preflight actually merged

When the set contains a closed optional pin and at least one other pin merges, the loop above skips the optional pin, but this invocation reloads the entire pr-set.json. pin_contract.py consequently checks code that was deliberately not merged and reports it missing, turning an otherwise successful preflight into a failure and triggering the repin automation. Pass an effective pin list here, as the nightly does with --prs-json.

Useful? React with 👍 / 👎.

Comment on lines +166 to +168
stripped = text.strip()
if len(stripped) >= MIN_LINE and not TRIVIAL_RE.match(stripped):
lines[cur].append(stripped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Strip inline comments before recording contract lines

When a pin adds a substantial code line with a trailing comment and a later valid resolution changes only that comment, the contract still contains the original comment because the line is appended before COMMENT_RE is applied. With the default threshold of 1.0, this rejects a merged tree whose executable code is unchanged, despite the stated policy that comments are not part of the contract.

Useful? React with 👍 / 👎.

Comment on lines +244 to +245
text = target.read_text(errors="replace")
lost = [w for w in wanted if w not in text]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore commented-out text when checking survival

When a bad merge resolution comments out a pin-added statement rather than deleting it, the raw file still contains the wanted string, so w in text counts the disabled statement as surviving. Such a tree can compile while silently losing the feature, which is the exact failure this gate is intended to catch; compare against comment-stripped executable text instead.

Useful? React with 👍 / 👎.

danielhanchen pushed a commit that referenced this pull request Sep 3, 2026
The base tag moved from b10775 to b10786 while this was open, and five
pins needed work to survive it. Four of the five merge without a single
conflict and produce a tree that does not compile, which is the failure
mode the compile gate in #175 exists for; three of those four are the
same upstream change.

- #172 inkling, #173 glm5next, #177 diffusion-gemma: n_ff_exp became a
  per-layer array behind an accessor. Reading the old scalar field is a
  compile error, and inside a create_tensor dimension list the member
  function quietly decays to a pointer-to-member instead. #172 and #173
  also override preprocess(), which the mtmd base classes made const.
- #152 per-run buffers: b10786 added a load-ordering pass that reads a
  llama_buf_map entry as one buffer, and this pin made an entry a run of
  buffers.
- #144 qwen4exp MTP: the only one that conflicts, in both places it
  touches, over the same n_ff_exp change.

Verified on b10786: all 13 pins merge (11 clean, 2 additive), the CPU
llama target builds, and the CUDA build plus the feature matrix are in
the PR comment.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c21aa4324c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +193 to +194
-DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_TOOLS=OFF -DLLAMA_CURL=OFF > /dev/null \
|| ! cmake --build "${RUNNER_TEMP}/gate" --target llama -j "$(nproc)" ; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Build mtmd in the preflight compile gate

When a pin changes tools/mtmd and only that target fails to compile, this preflight remains green because it disables tools and builds only llama, while the compile gate in .github/workflows/unsloth-prebuilt.yml explicitly builds both llama and mtmd. This defeats the earlier preflight for the exact class of mtmd failure described in the nightly workflow, so the problem is discovered only when the nightly starts. CMake's local help confirms the semantics: --target <tgt>... builds the named targets instead of the defaults.

Useful? React with 👍 / 👎.

Comment on lines +240 to +243
for path, wanted in sorted(contract["lines"].items()):
target = root / path
if not target.is_file():
continue # already reported, or a deletion

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report missing files that the pin modified

When a bad resolution deletes a pre-existing file that the pin modified, and that file contains no identifier from SYMBOL_FAMILIES, this branch skips all of its line-survival checks. Only files classified as newly added are reported above, so the comment's “already reported” assumption is false for modified files and the pin can pass despite losing every line it contributed to that file.

Useful? React with 👍 / 👎.

Comment on lines +324 to +327
if total and hit / total >= args.redundant_at:
note = (f"the base tag already has {hit}/{total} ({hit / total:.0%}) of the "
"lines this pin adds; upstream has taken this work and the entry "
"should be deleted from pr-set.json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect pins already contained in the base

When upstream merged the pinned SHA without squashing and a later base contains it as an ancestor, merge-base(pin, base) is the pin itself, so the derived diff has zero lines and total is zero. This condition then suppresses the retirement notice while check() has an empty contract and passes, leaving exactly the stale, no-op pin this checker says it should identify; handle ancestry or an empty contract before requiring a nonzero line total.

Useful? React with 👍 / 👎.

@danielhanchen
danielhanchen merged commit ef879e6 into master Sep 4, 2026
4 checks passed
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.

1 participant