Skip to content

fix(kb): guard warm-start replay on the shape the canonical_id cannot express - #1495

Merged
ZhengGong-amd merged 1 commit into
mainfrom
fix/rpoornac/warm-start-shape-guard
Sep 16, 2026
Merged

ZhengGong-amd merged 1 commit into
mainfrom
fix/rpoornac/warm-start-shape-guard

Conversation

@rpoornac

@rpoornac rpoornac commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

The gap

The recipe canonical_id is a seven-tuple:

inference:{model}:{hardware}:{framework_name}:{model_type}:{architectures}:{framework_version}:{precision}

Expert parallelism is not a dimension of it, and neither is the compute-partition mode. Before this change kb_hardware_slug appended topology only when nodes >= 2, so on a single node it returned the bare GPU type. A run in SPX and a run in CPX produced the same id — and recipe_canonical_id has no partition parameter, so the difference was not merely unrecorded, it was inexpressible.

Nothing downstream caught it either. The warm-start cascade relaxes only conc/isl/osl, and RecipeScope.matches_workload_shape compares a hardcoded (tp, conc, isl, osl). So an exact tier hit at confidence 1.0 could hand the PRELUDE auto-replay a config recorded with eight times the partitions. --warm-replay-min-reproduce-pct is a real backstop and the run does recover, but only after paying for the verify round — and status=drift reports it as a recipe that failed to reproduce rather than as a machine that never matched.

What this changes

The approach is to make the collision unrepresentable rather than detectable. An earlier revision of this PR compared shapes at read time and demoted disagreeing rows; that was blocked on mechanism and removed — see Why identity below.

The shape is part of the hardware identity. kb_hardware_slug takes a partition_mode and encodes it, plus expert parallelism, at any node count:

MI300X_cpx                     <- CPX pod
MI300X_dpx                     <- DPX pod
MI300X_ep8                     <- single node, EP=8
MI300X_ws16_tp8_ep8_cpx        <- 2 nodes, EP=8, CPX

The default shape is no shape. SPX and ep <= 1 append nothing, so every key written before this change is byte-identical after it:

MI300X                         <- whole card, dense (the historical key)
MI300X                         <- no mode published at all
MI300X                         <- EP=1 is dense, not a shape

One resolver feeds both sides. resolve_kb_topology now returns partition_mode, read from the state's compute_partition["mode"] and falling back to published_shape(). The KB reader and the writer derive the key from that one call, so they cannot drift into agreeing only by coincidence.

The fallback tiers carry the shape. _hardware_fallback_values preserves the topology suffix, so a partitioned pod reaches same-ISA siblings at its own shape and never the whole-card row at any tier:

>>> _hardware_fallback_values("MI300X_cpx")
['mi300x_cpx', 'mi308x_cpx', 'mi325x_cpx']
>>> _hardware_is_compatible("MI300X_cpx", "MI300X")
False

_TOPOLOGY_SUFFIX_RE is extended so a shaped single-node slug still parses, without admitting arbitrary suffixes:

MI300X_cpx                     -> '_cpx'
MI300X_ep8_cpx                 -> '_ep8_cpx'
MI300X_ws16_tp8_ep8_cpx_sglang -> '_ws16_tp8_ep8_cpx_sglang'
MI300X_garbage                 -> ''

The read-side guard is deleted, along with _shape_mismatch, _GUARDED_SHAPE_KEYS, the strict_shape parameter, and --recipe-kb-strict-fingerprint — a flag that was declared in the parser and read nowhere.

Why identity, not a read-side check

@ZhengGong-amd blocked the earlier revision, and all three defects reproduced:

  1. The comparison read a dict that cannot hold the keys. _with_exact_history builds a fixed ten-key mapping with no tp, ep or partitions in it, so the check reported a mismatch for dimensions the dict is structurally incapable of carrying: 'tp: row=None pod=8, ep: row=None pod=8, partitions: row=None pod=8'.
  2. Demotion did not deny the replay. Setting warm_tier/warm_conf to seed_only/0.0 leaves the config-donor block reachable, because it is entered on warm_point being truthy. _donor_is_trustworthy then compares only conc/isl/osl and never looks at the demoted dimensions, so the config replayed anyway — a guard that logged a refusal and handed over the config.
  3. DEFAULT_EP = 1 demoted the whole corpus. Every dense CLI run has state.ep == 1, so workload_shape published ep: 1 on every run and the check reported 'ep: row=None pod=1' against every historical row.

The first and third are consequences of comparing at read time, not implementation slips. Putting the distinction in the key removes the comparison altogether.

It also closes @meinali-566's point without a separate change: RecipeScope had no way to filter on ep or partitions, and distinct canonical_ids mean there is nothing left to filter.

Blast radius

Against a historical row that recorded no shape at all:

Pod Outcome
dense, no partition env kept — key byte-identical
dense, SPX published kept — byte-identical
dense or MoE with ep=1 kept — ep=1 is not a shape
DPX published not reached (pod key MI300X_dpx)
CPX published not reached (MI300X_cpx)
MoE, EP=8 not reached (MI300X_ep8)

A partitioned pod now misses rather than mis-hits. It falls through the normal cascade and, absent a row recorded at its shape, seeds cold — the honest outcome for a corpus that predates the distinction, and it self-heals as sessions republish under the shaped key. The previous revision's table promised demotions here; that was the mechanism that got removed.

Scope

No value{} schema change and no allowlist change: workload_shape is an existing published key, sanitize_shared_knowledge filters secrets and host paths rather than structural keys, and RecipeScope.as_dict() — the Store query payload — is untouched.

workload_shape still publishes ep and partitions for disclosure, now omitting a dense ep and preferring the partition count the run actually published over one re-derived from the mode. Nothing compares those fields; they are there to be read by a human and by the prompt.

Verification

  • 14 new tests: slug encoding for divided vs. whole cards, single-node EP, SPX treated as no shape; resolve_kb_topology carrying the mode, preferring a persisted mode on resume, and leaving it unset when nobody published one; a partitioned pod not reaching a whole-card row, a whole-card pod still reaching the row it recorded, SPX staying identical to an unrecorded mode; workload_shape publication, SPX and dense-ep omission, published partition count winning; and projection completeness against the publisher.
  • 264 passed across test_recipe_kb_t0_anchor, test_canonical_id_5tuple, test_multi_node_scripts and test_remote_recipe_v2.
  • CI green: 30 checks passing, 0 failing.
  • ruff check and ruff format --check clean.
  • +412/−27 across 10 files: +260 tests, +49 CHANGELOG, and +103/−24 of business code.

Follow-up

rocm and aiter are still written into every row's stack_fingerprint and compared nowhere at read time. That is a real gap and it is tracked in #1507, with the prerequisite recording fix in #1508. It is deliberately out of scope here: ROCm and AITER cannot go into the identity on these terms, because every patch bump would mint a fresh canonical_id and no row would ever be reusable. Defect 2 above is the specific trap that design has to avoid.

@rpoornac
rpoornac requested a review from a team as a code owner September 11, 2026 22:30
@rpoornac
rpoornac force-pushed the fix/rpoornac/warm-start-shape-guard branch from 4a2c251 to aa5d189 Compare September 13, 2026 01:07
@rpoornac
rpoornac force-pushed the fix/rpoornac/warm-start-shape-guard branch from aa5d189 to 59076b0 Compare September 14, 2026 04:56
@ZhengGong-amd

Copy link
Copy Markdown
Collaborator

Blocking: the shape belongs in the identity, not in a read-side guard

The diagnosis is right and worth fixing — SPX and CPX really do share one canonical_id. The mechanism is what I'm blocking on: three of the guard's behaviours are wrong on the main paths today (all green under the new tests), and the rule it implements already exists in this repo.

Required change

kb_hardware_slug already states this PR's argument verbatim:

_tp{tp} / _ep{ep} — the parallel formation. These are fixed at launch (not explored), so a best_config tuned at one split (e.g. TP4 vs TP2/DP2 over the same world size) is invalid at another […] ep==1 (no expert parallelism) is omitted so dense keys stay clean.

It is gated to nodes >= 2 only to keep single-node keys byte-identical — which is the same "omit the default so historical rows don't move" rule you applied to SPX. Apply it in that one place:

  1. In kb_hardware_slug, let single-node participate in suffixing, but only for a non-default shape: SPX or unpublished mode → no suffix; ep <= 1 → no suffix; DPX/QPX/CPX → append the mode. Have resolve_kb_topology read the mode from state.compute_partition / HYPERLOOM_PARTITION_*.
  2. Delete _shape_mismatch, _GUARDED_SHAPE_KEYS, the strict_shape parameter, the cli/kb.py passthrough, and --recipe-kb-strict-fingerprint itself (record the removal in the CHANGELOG).
  3. Keep the workload_shape publication and the SHAPE_KEYS derivation in knowledge_to_warm_recipe — that part is right. Two fixes: omit ep <= 1 for the same reason you omit SPX, and prefer state.compute_partition["partitions"] (already populated by published_shape() from HYPERLOOM_PARTITION_COUNT) over re-deriving the count from the mode table.

What that buys, and why it is the only shape that closes the issue: a CPX pod cannot read an SPX row because it is a different canonical_id — no flag, no demotion, no "a dimension neither side recorded" special case. Historical keys stay byte-identical, so nothing demotes corpus-wide. Read and write derive the key from one call, so the row a guard checked and the row that gets replayed cannot be different rows. And _hardware_is_compatible already requires the topology suffix to match exactly, so _find_config_donor is covered by construction rather than by a second check.

Please leave TP out of this PR: encoding tp single-node would re-key the existing corpus. Partition mode plus ep > 1 already covers the SPX/CPX case your description is built on, at zero migration cost. If TP needs guarding, add a key to the _shape_matches tuple already inside _donor_is_trustworthy in a separate PR, so the self and borrowed paths share one gate.

Why the guard cannot be patched into correctness

Three defects, each on a main path, each invisible to the six new tests.

1. _shape_mismatch reads a dict that structurally cannot hold shape keys. When a row carries exact_history, the comparison reads exact_history — but _with_exact_history builds a fixed 10-key dict (canonical_id, view, the five experiential lists, sessions, the two gain fields). No tp, no ep, no partitions, ever:

class S:
    tp = 8; ep = 8; conc = 64; isl = 1024; osl = 256
    baseline_workload_extra = {}
    compute_partition = {"mode": "CPX"}

row = {"tp": 8, "ep": 8, "partitions": 8,
       "exact_history": {"canonical_id": "x", "lessons": [], "pitfalls": []}}
_shape_mismatch(S(), row)
# 'tp: row=None pod=8, ep: row=None pod=8, partitions: row=None pod=8'

_shape_mismatch(S(), {k: v for k, v in row.items() if k != "exact_history"})
# ''

Same row, same pod, exact match — reported as three mismatches purely because history was attached. This is the common path, not an edge case: in local mode T0 writes its own anchor row before reading, that row is non-actionable on a first run, so _cascade_warm_start_search sets exact_history and every borrowed hit goes through this branch. The semantics are also inverted — exact_history supplies the priors, the top level supplies the config you intend to deny.

2. The demotion does not deny the replay. It only sets warm_tier/warm_conf; the config-donor block right below still runs (warm_point is truthy), and _find_config_donor returns a row gated only by _donor_is_trustworthy, which compares conc/isl/osl and never looks at tp/ep/partitions. Its confidence becomes recommended_replay.config_confidence directly:

warm-start row shape does not match this pod (partitions: row=8 pod=None); demoting exact to seed_only
tier      : seed_only
confidence: 0.0
replay    : {'extra_server_args': '--also-from-a-cpx-pod', ...,
             'config_tier': 'model_family', 'config_confidence': 0.9, ...}

0.9 clears the 0.7 default, so PRELUDE replays a config from a CPX pod anyway. The guard fires, logs, and hands the replay from a row it checked to a row it did not.

3. DEFAULT_EP = 1 invalidates the blast-radius table. --ep defaults to DEFAULT_EP = 1 via _int_arg("ep", DEFAULT_EP), so state.ep == 1 on every CLI run, and _positive_int(1) publishes it:

workload_shape(dense_run_state)
# {'tp': 8, 'ep': 1, 'conc': 64, 'isl': 1024, 'osl': 256}

_shape_mismatch(dense_run_state, {"tp": 8, "conc": 64, "isl": 1024, "osl": 256})
# 'ep: row=None pod=1'

So "ep is absent unless the run actually sets it" and the table's first row (dense, no partition env → kept) don't hold: under the flag, every row written before this PR demotes on every run. It's the false disagreement you carefully avoided for SPX, and kb_hardware_slug already documents the rule that prevents it.

All three are the same failure mode. The guard compares rows across publishers that disagree by construction: workload_shape, _build_t0_trace_extras and _collect_workload_tags each restate the shape key list, and they differ on partitions (only the first writes it, so it never self-heals on a local row), on pp (only the second writes it, and it is as replay-sensitive as tp/ep), and on the EP env fallback (the last two have it, the first doesn't — another false demotion). Add a dimension and all of it has to be revised again, with the next exact_history-shaped mistake equally invisible. Making the mismatch unrepresentable removes the class instead of the instance.

One note on the flag's original promise

It isn't vacuous. framework_version and precision are identity dimensions, but rocm_version and aiter_commit — both written into every row's stack_fingerprint — are compared nowhere at read time. So the M5 consumer still has a real job; it is just a different one. That argues for deleting this flag and filing that separately, not for keeping the name and swapping the semantics: a flag whose name says fingerprint and whose behaviour is workload shape turns an unimplemented promise into a misnamed one.

@meinali-566

Copy link
Copy Markdown
Contributor

@rpoornac Thanks for adding the ep / partitions publication and the replay-side safety guard. I think there is one KB Store selection gap worth clarifying.

RecipeScope already partitions the current KB Store write/read path by:

kernel_optimizer, tp, conc, isl, osl

The remote client sends scope.as_dict() to KB Store, validates the returned View’s scope, and separately validates knowledge.workload_shape through RecipeScope.matches_workload_shape().

Therefore, tp is already an exact Store-scope dimension on the current remote path. conc, isl, and osl are also represented in the scope and handled by the T0 relaxation cascade where appropriate.

This PR adds ep and partitions to workload_shape and checks:

("tp", "ep", "partitions")

after a warm row has already been returned. However, RecipeScope.as_dict() and the KB Store scope remain unchanged.

That is a useful safety backstop: it prevents an incompatible config from being replayed by demoting the row to seed_only. However, it does not allow KB Store to select the correct compatible Recipe in the first place.

For example, these two sessions can still share the same current Store scope:

A: tp=8, ep=1, partitions=1
B: tp=8, ep=8, partitions=8

If A is the scoped champion while the current pod matches B, the new guard safely demotes A. But the reader cannot continue to B because KB Store never partitions or queries candidates by ep and partitions.

Therefore, the current implementation solves:

Do not replay an incompatible Recipe.

but not yet:

Select the compatible Recipe from KB Store.

Would you agree that ep and partitions should also become RecipeScope / KB Store scope dimensions, while retaining the strict shape check as defense in depth?

If so, please confirm the intended compatibility behavior for historical rows, especially:

  • missing ep
  • missing partitions
  • omitted SPX / partitions=1

After confirmation, I can make the corresponding KB Store service, schema, and query changes.

I would also prefer to avoid introducing another parallel set of variables or mappings. Ideally, both the scope projection and replay guard should derive from the existing workload_shape() publisher/shared state as a single source of truth. conc, isl, and osl should continue using the existing RecipeScope and cascade behavior rather than being introduced again elsewhere.

The recipe canonical_id is a seven-tuple of model, hardware, framework
name, model type, architectures, framework version and precision. The
compute-partition mode is not among those dimensions, and neither is
expert parallelism on a single node, so kb_hardware_slug collapsed to the
bare GPU type and a run in SPX and a run in CPX shared one identity. The
warm-start cascade only relaxes conc/isl/osl, so an exact hit at
confidence 1.0 could hand the auto-replay a config recorded with eight
times the partitions, with --warm-replay-min-reproduce-pct noticing only
after the verify was spent.

kb_hardware_slug now suffixes the partition mode and ep at any node
count rather than only inside a cluster key. Both are fixed at launch
rather than explored, which is the argument _tp{tp} already makes for
itself, so a best_config tuned under one is invalid under the other. A
CPX pod therefore cannot reach an SPX row: it is asking a different
canonical_id. That removes the failure class instead of an instance of
it -- there is no second comparison that could be applied to a different
row than the one that ends up replayed, because resolve_kb_topology is
the single call both the reader and the writer build the key from, and
_hardware_is_compatible already requires the topology suffix to match
exactly, so the config-donor path is covered by construction.

Every suffix is omitted at its default value: ep <= 1 is dense, and SPX,
an unpublished mode and a mode this build does not recognise are all the
whole card. Existing keys stay byte-identical, so nothing in the corpus
moves. _TOPOLOGY_SUFFIX_RE learned the single-node forms as well, or
_hardware_fallback_values would have quietly stopped offering the
same-ISA SKUs for exactly the rows these suffixes were added for. tp is
deliberately left out: almost every single-node run sets it, so encoding
it would re-key the whole corpus rather than only the rows that collide.

workload_shape still publishes ep and partitions, now as a description
of the run rather than the gate on replaying it, and
knowledge_to_warm_recipe still derives its projection allowlist from the
publisher instead of restating it. Both are omitted at their default:
--ep defaults to 1, so publishing it would have every dense run claim a
formation it never chose, and one partition is the whole card. The count
a launch published wins over one re-derived from the mode name, so there
is only one derivation to keep in agreement.

--recipe-kb-strict-fingerprint is removed. It was declared in the parser
and read nowhere, and the stack_fingerprint disagreement it promised to
refuse was never the exposure, since framework version and precision are
already identity dimensions. With the mode in the key there is nothing
left for a read-side comparison to do. rocm_version and aiter_commit are
written into every row's stack_fingerprint and compared nowhere at read
time; that gap is real and is tracked separately rather than under a
flag whose name says fingerprint and whose behaviour would have been
workload shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rpoornac
rpoornac force-pushed the fix/rpoornac/warm-start-shape-guard branch from 59076b0 to 1734757 Compare September 14, 2026 16:23
@rpoornac

Copy link
Copy Markdown
Collaborator Author

@ZhengGong-amd Agreed on all three, and I reproduced each one before changing anything rather than taking them on faith. Pushed as 1734757.

The three defects, confirmed. _shape_mismatch on a row carrying exact_history reported tp: row=None pod=8, ep: row=None pod=8, partitions: row=None pod=8 for a row that matched the pod exactly, and '' for the same row with exact_history stripped — _with_exact_history builds a fixed 10-key dict, so the shape keys could never be in the dict I was reading. The demotion also did not deny the replay: it sets warm_tier/warm_conf and returns, and the config-donor block below is still entered because warm_point is truthy, with _donor_is_trustworthy comparing only conc/isl/osl. And DEFAULT_EP = 1 means state.ep == 1 on every CLI run, so workload_shape published {'tp': 8, 'ep': 1, ...} and every pre-PR row produced ep: row=None pod=1 — the mass demotion I had carefully avoided for SPX, reintroduced one dimension over.

What the PR does now. kb_hardware_slug suffixes the partition mode and ep at any node count, so the collision is unrepresentable rather than detected: MI300X vs MI300X_ep8_cpx are different identities and a CPX pod never reaches an SPX row. _shape_mismatch, _GUARDED_SHAPE_KEYS, strict_shape, the cli/kb.py passthrough and --recipe-kb-strict-fingerprint are all gone, with the removal recorded in the CHANGELOG under ### Removed. workload_shape and the SHAPE_KEYS derivation stay, now omitting ep <= 1 and preferring compute_partition["partitions"] over re-deriving from the mode table. TP is left out, for the reason you gave. Verified that the default shape is byte-identical — nodes=1 with tp=8, ep=1, partition_mode="SPX" still returns exactly MI300X, as does an unrecognised mode.

One companion change you did not ask for, because it would otherwise have regressed silently. _TOPOLOGY_SUFFIX_RE required a mandatory leading _ws[1-9]\d*, so a single-node _cpx or _ep8 suffix failed to parse, _parse_hardware_topology returned None, and _hardware_fallback_values stopped offering the same-ISA SKUs for exactly the rows these suffixes were added for. I extended it with an alternation that keeps _ws mandatory for the cluster form, so MI300X_cpx now yields ['mi300x_cpx', 'mi308x_cpx', 'mi325x_cpx'] while MI300X_garbage still fails to parse rather than being mistaken for a backend name. _hardware_is_compatible("MI300X_cpx", "MI300X") is False, which is the original bug closed at the identity level.

On the flag's original promise: you are right that it is not vacuous, and rocm_version / aiter_commit being written into every stack_fingerprint and compared nowhere at read time is a real gap. I have noted it in the CHANGELOG as tracked separately and will file it, rather than keeping a flag whose name says fingerprint and whose behaviour is workload shape.

Full suite on the new base: 22113 passed, 9 failed — all nine reproduce on a clean main worktree (test_external_multi_node, test_agentx_repair, test_preflight_auth_override, and two load-flaky test_supervisor cases), none in the KB or recipe paths. ruff check and ruff format --check clean.

@rpoornac

Copy link
Copy Markdown
Collaborator Author

@meinali-566 Thanks — the selection gap you describe was real, and your example is exactly the right one. The answer changed with the rework I just pushed (1734757), so let me answer against the new shape rather than the one you reviewed.

Short answer: no new RecipeScope dimensions are needed for this, because the two sessions in your example no longer share an identity. The mode and ep now suffix the hardware slug in kb_hardware_slug, so your A and B are:

A: tp=8, ep=1, partitions=1  ->  inference:{model}:mi300x:...
B: tp=8, ep=8, partitions=8  ->  inference:{model}:mi300x_ep8_cpx:...

A pod matching B queries B's canonical_id directly, so KB Store selects the compatible Recipe by identity — there is no "cannot continue to B", because B was never behind A. This also means the strict shape check is deleted rather than retained as defense in depth: with the mismatch unrepresentable, a read-side comparison has nothing left to refuse, and keeping one would mean a second place that has to agree about the shape. That was the substance of ZhengGong's block, and I think it dissolves your gap as a side effect rather than deferring it.

Your three compatibility questions, explicitly. All three cases are deliberately indistinguishable from each other and all three keep their existing key:

historical row treated as key
missing ep dense (ep <= 1) unchanged
missing partitions whole card unchanged
omitted SPX / partitions=1 whole card unchanged

A mode this build does not recognise also falls in that bucket, so an unknown string cannot invent a key. I verified the byte-identical property directly: kb_hardware_slug("MI300X", nodes=1, tp=8, ep=1, partition_mode="SPX") is exactly "MI300X".

One honest limitation, since you asked specifically about historical rows. This prevents future collisions; it does not retroactively re-file rows already written under a colliding key. A single-node ep=8 row recorded before this change still sits under the bare MI300X key, so a dense pod can still reach it. Re-keying it would require a migration pass over the corpus, which I have deliberately kept out of this PR.

On your last point, we agree, and that is how it is built: workload_shape() remains the single publisher, SHAPE_KEYS is derived from it so the warm-row projection cannot drop a dimension the publisher emits, and the key derives from one resolve_kb_topology() call that both the reader and the writer go through. conc/isl/osl keep the existing RecipeScope and cascade behaviour untouched — nothing about them moved.

So I do not think there is KB Store service, schema or query work needed for this issue. If you would still like ep/partitions as first-class Store scope dimensions for a separate reason — querying or reporting across formations, say — that seems worth doing on its own terms, and workload_shape() is the right source to project from.

@rpoornac

Copy link
Copy Markdown
Collaborator Author

Housekeeping: I've rewritten the PR description, which was still describing the previous revision's read-side guard — the --recipe-kb-strict-fingerprint flag, the seed_only/0.0 demotion, and a blast-radius table promising demotions. None of that is in the branch anymore. The description now matches what the code does (shape in the hardware identity, so a partitioned pod misses rather than mis-hits), records the three defects and why read-time comparison was the wrong mechanism, and corrects the verification numbers: 14 new tests and +103/−24 of business code, not 6 and ~106.

No code changed — head is still 1734757 and CI is unchanged at 30 green. Apologies to anyone who read the old text from an email notification.

The out-of-scope rocm/aiter gap now has a number: #1507, with the recording prerequisite in #1508.

@meinali-566

Copy link
Copy Markdown
Contributor

@rpoornac I re-reviewed the latest revision (1734757) and agree with this approach.

And no other block issues are found.

LGTM cc @ZhengGong-amd

@ZhengGong-amd
ZhengGong-amd merged commit 31143ab into main Sep 16, 2026
32 checks passed
@ZhengGong-amd
ZhengGong-amd deleted the fix/rpoornac/warm-start-shape-guard branch September 16, 2026 01:57
lishuoshuo-amd added a commit that referenced this pull request Sep 16, 2026
The opening paragraph claimed "Nothing in the CLI, the environment contract, or
the session record moves". The last two hold; the first does not. An operator
reads that line to decide whether upgrading is safe, and three surfaces v1.1.0
accepted are gone in this tree.

Two are optimizer options, and the parser is strict on purpose
(cli/__init__.py), so neither is an ignored token: parse_args exits 2 with
"unrecognized arguments" before the session starts. --recipe-kb-strict-
fingerprint was removed in #1495; it was declared in the parser and read nowhere
in v1.1.0, so only the rejection is new. --breakdown-include-transcripts was
removed in #1455 along with the Session Breakdown section it inlined into, and
that one was live in v1.1.0. The third is the deprecated console-script alias,
which fails as "command not found" rather than a parser error -- a different
failure mode, so it is stated beside the table rather than inside it.

The highlights bullet no longer opens on a count: its three items are a
different three from the table's, and two "three"s meaning different sets is
worse than neither naming one.
@lishuoshuo-amd lishuoshuo-amd mentioned this pull request Sep 16, 2026
8 tasks
lishuoshuo-amd added a commit that referenced this pull request Sep 16, 2026
Picks up the three reverts (#1523, #1524, #1525), which drop #1520, #1511 and
#1495 out of the release.

#1525 is the one that changes this branch. #1495 both re-keyed the Recipe KB on
compute-partition shape and removed --recipe-kb-strict-fingerprint, so reverting
it restores the option: the parser carries it again, and the CLI delta against
v1.1.0 is now one removal (--breakdown-include-transcripts) and one addition
(--extend-hours). Its two changelog entries, which main deleted from
[Unreleased], are dropped from the v1.1.1 section here -- the conflict was the
whole section against an emptied [Unreleased], resolved by keeping the section
and deleting those two. The --breakdown-include-transcripts entry loses the
comparison it drew against the option that is now back.

The release notes lose the same two claims: the partition-key highlight, the
warm-start clause in the opening paragraph, and the upgrade-table row, leaving
one removed option rather than two. #1520 and #1511 needed no entries because
neither ships.

The environment contract is untouched by the reverts: the seven variables are
still absent from .env.template, and the five collective ones still have no read
site.
xiaofei-zheng pushed a commit that referenced this pull request Sep 16, 2026
* chore(release): bump version to 1.1.1

Every site that tracks the packaged version moves together: pyproject's
[project].version, the docs version_number kept in sync with it, the README
badge, the compatibility matrix row, and the three copy-paste
`pip install hyperloom-inference-optimizer==` pins, which would otherwise keep
installing 1.1.0 after 1.1.1 ships.

HYPERLOOM_WHEEL_TAG moves with it, which requires the v1.1.1 GitHub release to
carry the rocm-profiler-hotfix-libs.tar.gz asset the bare-metal installer
downloads from that tag.

Patch rather than minor: the release carries fixes, a vLLM default bump, and
removals of surfaces that were already unreachable -- no CLI, environment or
session-record change an operator has to plan for.

The TraceLens component version in the matrix is its own and stays at 1.0.0,
as do SOURCE_RESOLUTION_SCHEMA_VERSION and the sphinx dependency pin that
happen to read 1.1.0.

* docs(release): cut the 1.1.1 changelog section and release notes

The accumulated Unreleased entries become the 1.1.1 section, ordered
Removed/Changed/Fixed; the seven subsections the section had grown into are
merged into one of each, and the two consecutive vLLM default bumps are ordered
oldest first so the chain from 0.27.1 to 0.29.0 reads forward. No entry text
changes: all twelve entries are byte-identical to what they were under
Unreleased.

1.1.1 is a patch release, so the notes lead on what was corrected rather than on
new capability: a warm-start hit could replay a config measured on a
differently shaped card, the prior work a session had earned was not reaching
the model at all, agent-backend selection was answered three different ways,
and a watchdog restart was neither resumable nor bounded. The bare-metal vLLM
default and the three removed dead surfaces are named with their operator-
visible consequence.

The removal bullet points at CHANGELOG.md for the pre-rename console script
spelling rather than repeating it: test_no_stray_kernel_agents_references scans
every tracked file and exempts CHANGELOG.md as a historical record, but
docs/release-notes.md is not on that list.

* docs(release): say that the 1.1.1 command line does move

The opening paragraph claimed "Nothing in the CLI, the environment contract, or
the session record moves". The last two hold; the first does not. An operator
reads that line to decide whether upgrading is safe, and three surfaces v1.1.0
accepted are gone in this tree.

Two are optimizer options, and the parser is strict on purpose
(cli/__init__.py), so neither is an ignored token: parse_args exits 2 with
"unrecognized arguments" before the session starts. --recipe-kb-strict-
fingerprint was removed in #1495; it was declared in the parser and read nowhere
in v1.1.0, so only the rejection is new. --breakdown-include-transcripts was
removed in #1455 along with the Session Breakdown section it inlined into, and
that one was live in v1.1.0. The third is the deprecated console-script alias,
which fails as "command not found" rather than a parser error -- a different
failure mode, so it is stated beside the table rather than inside it.

The highlights bullet no longer opens on a count: its three items are a
different three from the table's, and two "three"s meaning different sets is
worse than neither naming one.

* docs(release): record the two CLI surfaces 1.1.1 ships undocumented

An AST comparison of the optimize parser against v1.1.0 finds exactly two
removed options and one added one. Only --recipe-kb-strict-fingerprint had an
entry, so the section under-reported the release on both sides.

--breakdown-include-transcripts goes under Removed. It is the more consequential
of the two removals and the one with no record at all: v1.1.0 read it
(cli/__init__.py), where --recipe-kb-strict-fingerprint's only occurrence
outside tests was its own parser declaration. #1455 retired the exported
specialist_runs section it inlined into, so nothing is left to inline;
transcripts still reach disk and travel as transcript_path.

--extend-hours opens an Added subsection the section did not have, restoring the
Removed/Added/Changed/Fixed order v1.1.0 used. It is the only way to continue a
run that has spent its budget, since elapsed time is summed forward across legs
and never reset.

The release notes gain the same option and lose a second false compatibility
claim of mine: "the environment contract ... does not move". It does -- seven
variables are gone from .env.template across #1424 and #1442, five of them with
no read site left in the tree. Those removals have no changelog entry either, so
rather than assert them in a summary the changelog does not back, the sentence
now claims only the session record, which is verified: LATEST_STATE_SCHEMA_VERSION
is 6 in both v1.1.0 and this tree.

The twelve entries inherited from Unreleased remain byte-identical; these two
are additions beside them.

* docs(release): file the Forge entries under the release that ships them, and record the env contract

Five entries sat under the published `## [v1.1.0]` heading that the v1.1.0 tag
does not contain -- 67 entries at the tag, 72 here, five added and none removed,
all from #1442. It branched after the release cut, when `[Unreleased]` was empty,
and appended to the nearest heading.

They describe 1.1.1 work, and the code says so rather than just the dates:
v1.1.0's kernelforge/config.py reads FORGE_AGENT_MODEL and KERNEL_AGENTS_MODEL at
line 217, and this tree reads neither. An operator on 1.1.0 has those variables
working. Leaving the entries where they were had 1.1.0 claiming a change its own
code contradicts, and hid the migration from the operators upgrading into it.
The five move verbatim into the matching v1.1.1 subsections; the v1.1.0 section
is now byte-identical to the tag apart from the "Current packaged version"
marker this branch moved to v1.1.1.

The collective lane's five environment variables get the entry they never had.
#1424 merged the lane into the rewrite controller and deleted the variables with
it. These fail unlike the removed options: config.py warns when
KERNEL_AGENTS_MAX_TURNS is still set, but none of these five is read or warned
about anywhere in the tree, so a launcher that still exports them runs with them
silently ignored. The changelog entry and the upgrade note both say so, since a
failure will not.

With FORGE_CLAUDE_MODEL / FORGE_CODEX_MODEL now documented by the moved #1442
entry, the release notes can state the environment contract instead of staying
silent about it: seven variables, in a table of their own because "nothing
refuses them" is the opposite of what the option table above it says.

The twelve entries inherited from Unreleased remain byte-identical. The file
gains three entries in total, the three authored here.

* docs(release): name the two silent migrations the upgrade section left out

Both are changes the changelog marks BREAKING, and both are the silent kind the
"Before upgrading" section exists for.

FORGE_AGENT_MODEL joins the removed-variable table. The changelog names four
model variables; the table carried two, and the count said seven because it was
adding five collective variables to those two. FORGE_AGENT_MODEL was the
provider-neutral rung and v1.1.0:kernelforge/config.py:217 read it, so a
deployment can be sitting on it now; it falls through to the provider default
rather than failing. KERNEL_AGENTS_MODEL stays out, since nothing in either
repository ever set it, so the count is eight. All eight are verified read at
v1.1.0 and unread here.

HYPERLOOM_REASONING_EFFORT gets a paragraph of its own because it was not
removed -- its accepted set shrank, so a removed-variable table cannot hold it.
v1.1.0 took minimal | low | medium | high; the ladder is now
low | medium | high | xhigh | max. The two readers disagree about a value
outside it, which is the reason to state it here rather than leave it to the
changelog: kernelforge's resolve_agent_reasoning_effort raises
"'minimal' is not a reasoning effort", while apply_reasoning_effort drops the
field and takes the gateway default, which is deeper and more expensive than
minimal was. Half the run refuses, half of it silently gets costlier.
@ZhengGong-amd
ZhengGong-amd restored the fix/rpoornac/warm-start-shape-guard branch September 16, 2026 10:44
@rpoornac

Copy link
Copy Markdown
Collaborator Author

Re-landed as #1542, at @ZhengGong-amd's recommendation, now that 1.1.1 is cut.

Restored by reverting the revert onto current main rather than rebuilding by hand, so this is the reviewed content: eight of the nine source files are byte-identical to 31143ab. parser.py differs only because main has since refactored --max-hours into DEFAULT_MAX_HOURS, and against current main the only change to that file is still the --recipe-kb-strict-fingerprint removal. The changelog entries moved into the [Unreleased] section the release opened, and the "tracked separately" line now names #1507.

ZhengGong-amd pushed a commit that referenced this pull request Sep 17, 2026
…1542)

Re-land of #1495, which was merged as 31143ab and reverted in #1525 to
stabilise the 1.1.1 release rather than for any defect: two unrelated
PRs were reverted in the same five minutes, the release PR names all
three together, and main's CI was green with the change in it.

Restored by reverting the revert onto current main, so the content is
the reviewed content. Eight of the nine source files are byte-identical
to what was merged. parser.py differs only because main has since
refactored --max-hours into DEFAULT_MAX_HOURS; relative to current main
this change still only removes the --recipe-kb-strict-fingerprint block.

The changelog entries moved into the new [Unreleased] section that the
1.1.1 cut opened, and the "tracked separately" promise in the Removed
entry now names the issue it refers to, #1507.

Verified on the new base: 264 passed across the four affected suites,
including the 14 tests that define the change, and ruff clean. No file
overlap with the 21 commits main gained, and #1512's fuzzy KB fallback
is in src/kernelforge, a different KB from the recipe KB this touches.

Co-authored-by: Cursor <cursoragent@cursor.com>
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