diff --git a/CHECKLIST-ship-topology-and-queues.md b/CHECKLIST-ship-topology-and-queues.md index bbed10ee1..7060a66e7 100644 --- a/CHECKLIST-ship-topology-and-queues.md +++ b/CHECKLIST-ship-topology-and-queues.md @@ -9,9 +9,10 @@ against and other people can run it on hardware this workspace does not own. Both crates are published: `windows-topology-sys` 0.2.0 and `windows-waitable-queues` 0.1.0 reached crates.io on 2026-09-05, neither yanked. They did not arrive by the route below. PR #56 was opened from `mikegrier/deferred-namespace-ops` on 2026-08-31 and **closed unmerged on 2026-09-15**; the -content is landing in peeled pieces instead, the first of which merged as PR #94, and this branch is -the second. The milestones below were written while #56 was open and describe a path that was not -taken, so M4 still reads as though nothing has shipped -- SH-4.15 owns reconciling it. +content is landing in peeled pieces instead: the first merged as PR #94 and the second as +PR #95, both of which this branch has since merged back. The milestones below were written +while #56 was open and describe a path that was not taken, so M4 still reads as though +nothing has shipped -- SH-4.15 owns reconciling it. **Milestone numbers are not a running order.** M7 through M15 are *review rounds on PR #56*, so they happened **inside M3**, between the pull request opening and a merge that never came. Reading the diff --git a/PLANS.md b/PLANS.md index a3dcb1b1c..a972e82bf 100644 --- a/PLANS.md +++ b/PLANS.md @@ -19,6 +19,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | +| [crates/topology-planner/CHECKLIST.md](crates/topology-planner/CHECKLIST.md) | in progress | **Planned, not built** -- the directory holds a plan and no code, and becomes a crate when M2 begins. Owns the mapping from a stated **goal** plus an abstracted idealized machine description to a set of execution domains: which processors host a domain, where each thread pins, which memory node it allocates from, what channel connects each pair, and where each channel's buffer lives. Filed because that mapping was **unowned**: [CHECKLIST-io-domains.md](CHECKLIST-io-domains.md) M32 lists the contracts "the runtime cannot be written without" and all of them concern the queue, while M33+.1 opens with "one pinned thread, its `IoRing`, its node-local registered pool, its shard" -- presupposing a plan nothing computed. Separate from `windows-topology-sys` because that crate states **facts** and this one applies **policy**; fusing them is what produced `outermost_partitioning_cache`, a policy answer sitting in the facts crate that three consumers then re-derived differently (SH-16.9). M1 was a *requirements* milestone -- it states what the topology must answer, and it fed the locality-model session, which has since concluded as `D-13`..`D-21`. **The component was deferred past PR #56 by direction**, contributing only planning documents there; #56 then closed unmerged on 2026-09-15 and its content is landing in peeled pieces instead, so the component is still unlanded and goes in its own pull request. Per `D-21` the topology reshape lands without it, since `windows-topology-sys` publishes a refined view of what the platform publishes and an adapter absorbs the rest. M2+ and M3+ are parked on that session concluding, and are additionally **awaiting a re-cut**: EP-D-4 and EP-D-5 re-scoped the component into four parts (`topology-model` holding the abstract machine description, the planner's traits and the plan type; `topology-planner`; an inward Windows adapter; an outward realizer), and only M1 has been reconciled with that. EP-1.1 is done and already earned its keep: checking the shard-set query against the model found `Processor::capacity` using `0` as both a valid efficiency class and a "not known" sentinel, which collide on every non-hybrid machine (filed as SH-16.12). | [crates/topology-planner/DESIGN-NOTES.md](crates/topology-planner/DESIGN-NOTES.md), [design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md](design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md) | | [CHECKLIST-io-domains.md](CHECKLIST-io-domains.md) | in progress | **M30 is complete and archived** in [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md): the queue crate's name, skeleton and SPSC shape. M31 built the bounded-array MPSC (with a lazily created manual-reset doorbell whose reset cannot be separated from the observation that there is nothing to take -- achieved by ordering plus a re-check rather than by a lock, per D-9 and D-15) and is done but for `M31.6`, the `loom` verification, which is re-homed as `M30.4` in [CHECKLIST.md](CHECKLIST.md). M32 remains: the contract decisions -- ordering, correlation, backpressure among them -- the domain runtime cannot be written without. M33+ parks the runtime itself, the creation-time-affinity thread builder, the namespace `Outcome` extension, the client-side `ThreadpoolWait` fan-in helper, and the durability crate. M-inf holds items each gated on a specific measurement rather than on taste. The N=1 path is the whole first deliverable and depends on no NUMA hardware. | [DESIGN-NOTES.md](DESIGN-NOTES.md), [DESIGN-SESSION-2026-08-30-numa-sharded-io-execution-domains.md](design-sessions/DESIGN-SESSION-2026-08-30-numa-sharded-io-execution-domains.md) | | [CHECKLIST.md](CHECKLIST.md) | in progress | M19: propagate the 2026-08-27 platform measurements (IoRing registration replaces the table; the completion-port/`IoRing` fork; `runs_long` as the growth mechanism; the measured 512 default maximum) into the crates whose code or documentation currently assumes otherwise. M20: decide the session-independent path form, now that path resolution is measured to follow the impersonated token's logon session. M21: reconcile with the impersonation and enumeration crates that landed during the session. M34 carries the review-driven repairs raised while shipping the placement tool: M34.1 (the reusable sabotage harness) is done, and M34.2 (route the placement tool's output through a sink rather than writing to stdout from many sites) and M34.3 (archive the completed item bodies still carried by the three root checklists) are open. M37: discharge the failable-call standard across the workspace. M30: find out how much of this workspace's algorithm correctness can be machine-checked -- a survey matching each argued-but-unchecked algorithm to a class of tool (TLA+/PlusCal, loom, bounded proof, `const` assertions), one pilot chosen because parameter shrinking makes an untestable property exhaustive, and a named list of what the pilot could not reach, which is the deliverable. Scoped as an instrument for narrowing hand-inspection rather than replacing it, and explicitly not a reversal of [D-31](crates/windows-waitable-queues/DESIGN-NOTES.md#d-31). Also re-homes `M31.6`, the `loom` verification the queue crate promises adopters before 1.0: it was previously untracked, referenced from that crate's design notes, a source file and its sabotage manifest with no live checklist item anywhere, and is now queued as M30.4. M30's rationale is in [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#machine-checking-what-is-argued) -- Tier 2, because no decision is taken yet; M30.5 is what produces one. | [DESIGN-NOTES.md](DESIGN-NOTES.md#remoting-synchronous-namespace-operations) for M19-M21 and M37; N/A for M30 | | [CHECKLIST-ship-topology-and-queues.md](CHECKLIST-ship-topology-and-queues.md) | in progress | Release `windows-topology-sys` 0.2.0 and `windows-waitable-queues` 0.1.0, which everything shorter-term depends on. **Both reached crates.io on 2026-09-05**, by a route this file does not describe, since PR #56 closed unmerged; `SH-4.15` owns reconciling M4 with what shipped. Deliberately redundant with [CHECKLIST-io-domains.md](CHECKLIST-io-domains.md): that file plans the design, this one plans the release, and a release has failure modes a design checklist does not surface. Two were found while writing it -- `windows-waitable-queues-v*` is missing from the publish workflow's tag list, so release-please would tag it and nothing would publish it, silently; and `windows-ioring-sys` is published against `windows-topology-sys = "0.1.0"`. (That second finding was later **corrected at SH-2.2**: the pin is a *dev*-dependency, which consumers never resolve, so it obliges a pin update but no release.) M1 settled the public surface before it was public (done, archived); M2 repairs the plumbing; M3 lands the branch; M4 releases; M5 verifies from outside the workspace; M6 is long-running validation and gates the queue crate's release specifically. M7-M13 were seven PR #56 review rounds (done, archived); M14, M15 and M16 are the three later rounds and carry the file's open work -- M15 owns the fix for an ABA hole that ships **disclosed rather than fixed**, so it does not block the release. M16 is the SH-3.1.1 diff review, the first to read the branch as a diff rather than react to a comment: seven findings, six fixed, including a publish-workflow regression this branch had introduced two commits earlier and a soundness hole in the crate about to freeze its API. Its remaining four are blocked on [design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md](design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md), which began by asking whether collapsing a seven-kind, any-depth topology onto a single cache boundary is the right projection and has since settled that presence and observation must be modeled rather than collapsed into an `Option`. **That work gated the merge, and has since discharged**: unlike M14 and M15, which concern a defect in an implementation that can ship disclosed, M16 concerned the shape of the public model `windows-topology-sys` 0.2.0 would publish, and a published model cannot be reshaped without another break. It became the `MMT-*` plan, which has landed; the session has concluded and 0.2.0 shipped the new model, so M3 no longer waits on M16. The file opens with a status table. | [crates/windows-topology-sys/DESIGN-NOTES.md](crates/windows-topology-sys/DESIGN-NOTES.md), [crates/windows-waitable-queues/DESIGN-NOTES.md](crates/windows-waitable-queues/DESIGN-NOTES.md) | diff --git a/crates/topology-planner/CHECKLIST.md b/crates/topology-planner/CHECKLIST.md new file mode 100644 index 000000000..3273e095f --- /dev/null +++ b/crates/topology-planner/CHECKLIST.md @@ -0,0 +1,221 @@ +# Checklist: the topology planner + +Plans an arrangement of execution domains from a stated **goal** plus an **abstracted idealized** +description of a machine. See [COMPONENT.md](COMPONENT.md) for what this crate is and why it is +separate from both the topology crate and the runtime, and +[EP-D-4](DESIGN-NOTES.md#ep-d-4) for the architecture it now sits in. + +**The component has been re-scoped**, per [EP-D-4](DESIGN-NOTES.md#ep-d-4) and +[EP-D-5](DESIGN-NOTES.md#ep-d-5). It is named `topology-planner` and the directory now matches; it +queries an abstract model covering processors, memory, storage, interconnects, distances and +bottlenecks rather than `MachineMemoryTopology` directly; and **adapters** bracket it -- one exposing +the model's traits over the Windows topology objects, one realizing a plan as buffers, rings and +threads. The model, its traits, and the plan type live in a separate `topology-model` crate that +everything depends on and that depends on nothing. +**M2+ onward are written against the superseded shape and are not yet re-cut.** + +## Where this stands + +**Nothing is implemented.** M1 is the only active milestone, and it is deliberately a +*requirements* milestone rather than an implementation one: its output is the concrete statement +of what the model must answer, which the open design session needs in order to settle it. + +**Deferred past PR #56, by direction.** This component contributes only planning documents to that +PR and no code. `windows-topology-sys`'s reshape lands there without it: per +[D-21](../windows-topology-sys/DESIGN-NOTES.md#d-21) that crate publishes a refined view of what the +platform publishes, and an **adapter** absorbs whatever this component needs beyond it, so the two +are no longer coupled. + +The design session that gated M2 onward **has concluded** -- its questions were answered as `D-13` +through `D-21`, and the `MMT-*` plan is what it produced. M2+ is now gated on this component's own +prerequisites rather than on someone else's decision. + +| Milestone | State | What it is waiting on | +|---|---|---| +| M1 the input contract | 4 done, 1 open | `EP-1.5`'s coverage half, which wants a settled model | +| M1+ scenario and naming | **partly answered** | the name is settled (EP-D-4); the goal input is deferred for litigation, by direction | +| M2+ the plan as a value | parked, **and needs re-cutting** | re-cut against EP-D-4/EP-D-5, then the topology reshape landing | +| M3+ the policies | parked | M2+ | +| M-inf parked | ungated | not scheduled, deliberately | + +## M1: state what the planner needs from the topology + +The point of doing this first: the design session asks what representation is most useful to +consumers, and **this crate is the consumer**. Answering in the abstract has already produced one +wrong answer this session. Each item below states a query the planner makes, why it makes it, and +whether the topology can answer it today -- so the model is designed against a real caller. + +- [x] **EP-1.1** -- **The shard-set query.** Which processors may host a domain: online, with + identity carried as `(group, number)` rather than a bare number, with efficiency class and SMT + structure available so a policy can choose one domain per core or per thread and can decide + whether efficiency cores are peers. **Gap already identified:** parked and allocated state is not + available at all, and pinning a domain to a parked processor is a defect a client cannot detect. + Tracked as `SH-16.10`. + **Done:** stated as [EP-D-1](DESIGN-NOTES.md#ep-d-1), with each of its five inputs checked against + the model rather than assumed. Three are answered cleanly; availability is not answered at all; + and the fourth turned up a defect the item had not anticipated. + **`Processor::capacity` is unsafe for reading efficiency class.** It is + `online.then(find owning Core).flatten().unwrap_or(0)`, so `0` means offline, *or* in no core + domain, *or* genuinely class zero -- and the third is every processor on every non-hybrid machine, + so the sentinel collides with the common legitimate value. Worse here than elsewhere, because + Windows orders class `0` as *least* performant: on a hybrid part an unknown processor is + indistinguishable from an efficiency core, so a policy excluding them silently drops a possible + performance core and a policy tiering them mis-tiers it. Neither fails a functional test. Filed + against the owning crate as `SH-16.12`; use `DomainKind::Core { efficiency_class }` meanwhile. + +- [x] **EP-1.2** -- **The proximity query, which is the crux.** For an ~~*ordered pair*~~ + **unordered pair** of processors, how close are they -- because that is what chooses SPSC versus + MPSC versus a routed hop, and it is asked once per pair rather than once per machine. **The + current model cannot answer it**: `outermost_partitioning_cache` reports one global level and + `same_cache_domain` reduces it to a boolean at that level, so a client reconstructs the rest and, + per `SH-16.9`, reconstructs it differently each time. State the query precisely enough that the + session can design against it. + **Done:** stated as [EP-D-2](DESIGN-NOTES.md#ep-d-2). + **This item said "ordered pair" and was wrong**, corrected in place rather than quietly. The + repository had already settled it: `windows-placement-probe` documents that its placement labels + are "deliberately symmetric", that "the *relationship* between two processors genuinely is + symmetric", and that "direction therefore lives where it is real, not in the label" -- with the + measured side putting it as "a hop is not symmetric even though the link is". Proximity is the + link and is unordered; direction is the hop, and belongs to EP-1.3's residency question. + Three requirements came out of stating it. The answer needs the **membership** of the shared + granularity, not just its identity, or the planner re-derives the grouping to size an MPSC + fan-in. It needs to distinguish "tightest shared is X" from "**at most** X, and finer was not + observed", since under the model's bar the planner cannot go and check. And the order being by + inclusion rather than by firmware numbering means two granularities can be **incomparable**, so + the answer is a set of minimal shared granularities -- almost always one, but not by construction. + +- [x] **EP-1.3** -- **The residency query.** Which memory domain each processor belongs to, and -- + for a pair spanning two of them -- what it costs to place a shared buffer on one side rather than + the other. **Gap already identified:** `MachineMemoryTopology::distances` exists, is never populated, and Win32 + cannot populate it; the measurement exists in `windows-placement-probe` and reaches nothing. + Tracked as `SH-16.11`. The probe measures this per node pair with a dedicated ring-placement + column precisely because it was found to matter. + **Done:** stated as [EP-D-3](DESIGN-NOTES.md#ep-d-3). This is where the direction EP-1.2 refused + lands -- proximity is the link and symmetric, residency is the hop and is not. + The processor-to-node half is answered, with one asymmetry worth preserving: an unknown *cache* + domain costs an optimisation, but an unknown *memory* domain has no honest fallback, since the + pool must be allocated somewhere and guessing means quietly allocating remote memory for the life + of the process. `windows-placement-probe` already refuses on the second while tolerating the + first, and that judgement was correct. + **The cost half needs SH-16.11 restated, and it was.** That item read as though someone had + forgotten to populate a field. Two sharper problems replace it: `distances` can never carry + `Measured` provenance **by construction** -- its only inputs are a literal (`Synthetic`) and a file + (capped at `Restored`) -- so populating it would not help; and even populated it is SLIT-shaped, + one symmetric workload-independent scalar, while the question is directional. `D-9` in the + topology crate already deferred the attributed edge list that would answer it, naming *asymmetry* + among what it would absorb, with the trigger being that a scalar "demonstrably mismodels a machine + somebody is tuning for" -- and this planner is that machine-tuner. + **The trigger is approached, not met**, and the gap is a measurement nobody here can take: both + development hosts are single-node, so every directional run prints "VACUOUS ON THIS MACHINE". + Recorded so D-9 is reopened on evidence rather than on argument. + +- [ ] **EP-1.4** -- **What the planner does with an unanswered query**, given the model's bar is + that it answers without further measurement. A fact that was not observed cannot be acquired at + planning time, so decide per query whether the planner degrades to a documented weaker policy, + refuses to plan, or emits a plan carrying an explicit "this was chosen without knowing X" marker. + The third is the only one that survives review of a plan by a human, which is one of the reasons + a plan is a value. + **BLOCKED, and not merely because it is downstream.** EP-1.1 through EP-1.3 push requirements + *into* the model's design, which is why they were worth doing against today's model and found real + defects in it. This item reads behaviour *out* of the model -- it asks what the planner does when + the answer is "not observed", a state the model cannot currently express reliably -- so doing it + now would be analysing a shape that does not exist yet. + **It is also a duplicate.** The design session's fourth open question, "what a consumer does when a + needed fact is `not measured`", is this same decision seen from the model's side; the two were + filed independently before anyone noticed. Taken separately they can disagree: a planner that + degrades in a way the model does not support, or a model offering a fallback no consumer wants. + Answer them together, in the session. + **Narrowed by [D-19](../windows-topology-sys/DESIGN-NOTES.md#d-19).** The item says "decide per + query", and that is now more work than the model requires. A subject the two sources genuinely + contested is one the unified view does not cover, which is indistinguishable from not-observed to a + consumer -- so this is one decision about one degradation path, not one per reason a fact is + missing. The three candidate behaviours are unchanged. + **And it is no longer a duplicate**, per [D-21](../windows-topology-sys/DESIGN-NOTES.md#d-21). + `windows-topology-sys` publishes a refined view of what the platform publishes; what a consumer + *does* with an unobserved fact is not a question about that view. That crate owes only that the + absence be representable and distinguishable, which its `M2+.5` implements. `MMT-1.3` is closed on + those grounds, so **this item is now this component's decision alone** -- there is nothing left to + take jointly, and it no longer blocks anything in the model crate. + +- [ ] **EP-1.5** -- **Hand the resulting requirements to the design session** as the consumer-side + input it asked for, and record in the session which of them the settled model answers and which + it deliberately does not. + **Half done, and split because the halves have different prerequisites.** The *handover* is + complete: the session now carries the three queries in a table, plus the four model properties that + follow from them -- a pairwise query must exist, the order must be total, an answer must be able to + be an upper bound, and a measured number must carry what it measured. That was the part the model + designer needs in front of them, and it did not depend on the model existing. + **One of the four has since been corrected**, and it is recorded here rather than rewritten in the + session, which is an append-only record of what was handed over. "A pairwise query must exist" is + right about the requirement and wrong about the shape: per + [windows-topology-sys](../windows-topology-sys/COMPLETED-CHECKLIST.md) `M4+.1` the ordered collection is the + surface and the pairwise query is derived from it, because an answer obliged to carry the block + containing both processors is a question about the partition rather than about the pair. + The *coverage* half -- recording which requirements the settled model answers and which it + deliberately does not -- can only be written once there is a settled model. It stays open here. + > **-> CROSS-COMPONENT HANDOFF:** next work is in the repository root -> + > [DESIGN-SESSION-2026-09-02-cache-locality-model.md](../../design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md) + > -> `SH-16.8` in + > [CHECKLIST-ship-topology-and-queues.md](../../CHECKLIST-ship-topology-and-queues.md). + +## M1+: the scenario input, and the naming + +Raised when the engineer described this component's function, which turned out to be richer than +"takes a topology, applies policy". Both are gated on the locality-model session, but neither is a +model question -- they are this component's own. + +- [ ] **EP-1+.1** -- **Describe the scenario input.** The synthesizer takes *two* inputs and only one + is described anywhere. The scenario says what the caller intends to run, and it is what makes a + measurement meaningful: [EP-D-3](DESIGN-NOTES.md#ep-d-3) established that a measured number means + nothing without knowing what it measured, so at minimum the scenario must distinguish small-message + handoff from large-buffer streaming. Its absence is why "what is most useful for consumers" was + hard to answer in the abstract for so long. + +- [ ] **EP-1+.2** -- **Decide what the caller-callback traits ask.** Planning is a negotiation: the + component may call back for clarification the scenario did not settle. Enumerating those questions + is what decides whether this is one trait or several, and it cannot be done before EP-1+.1 says + what the scenario already answers. + +- [ ] **EP-1+.3** -- **Settle the naming, before any type is written.** Both inputs and the output + are graphs of processors and their relations, so "topology" fits all of them and distinguishes + none -- and a reader seeing the word twice will eventually take one for the other. Decide whether + the observed machine keeps the bare name (qualified only by its crate), gains a qualifier, or is + renamed outright, and what the synthesized arrangement is called. Cheap now; expensive once either + name is public. This one blocks nothing but should not be settled by whoever writes the first type. + +## M2+: the plan as a value + +Parked, not pending. Gated on the topology model landing. Shape recorded so it is not lost, per the +`M{n}+` convention. + +- [ ] **M2+.1** -- The plan type: domains, each with its processor, its memory domain and its + channels; inspectable and comparable, constructible against a synthetic topology so a machine + nobody has can be planned for and reviewed. + +- [ ] **M2+.2** -- Rendering a plan for a human to read before anything is pinned or allocated, + including which queries were unanswered and what was assumed in their place. + +- [ ] **M2+.3** -- Validation against synthetic topologies drawn from the shapes this repository has + actually met: the ARM64 host with no L3, the x64 host whose outermost partitioning cache is L2 + shared by SMT siblings, a hybrid part with efficiency classes, and a machine with more than 64 + processors so the group boundary is exercised rather than assumed. + +## M3+: the policies + +Parked. These are the choices the crate exists to make, and each is a decision item rather than an +implementation one. + +- [ ] **M3+.1** -- Domain-per-core versus domain-per-thread, and whether efficiency cores are peers, + excluded, or a second tier. + +- [ ] **M3+.2** -- The channel policy: what proximity justifies SPSC, what falls back to MPSC, and + whether any pair is deliberately not connected directly at all. + +- [ ] **M3+.3** -- Buffer residency for a channel spanning two memory domains, which the placement + probe already measures and which has no default that is right on both sides. + +## M-inf: parked, ungated + +- [ ] **M-inf.1** -- Re-planning at runtime, when processors are parked, hot-added, or the process + is given a different CPU-set allocation than it started with. Deliberately not scheduled: it needs + the static case to exist first, and it is a different problem. diff --git a/crates/topology-planner/COMPONENT.md b/crates/topology-planner/COMPONENT.md new file mode 100644 index 000000000..439bac093 --- /dev/null +++ b/crates/topology-planner/COMPONENT.md @@ -0,0 +1,123 @@ +# topology-planner + +**Planned, not built.** This directory currently holds a plan and no code. It becomes a crate +when [CHECKLIST.md](CHECKLIST.md) M2 begins; until then it exists so the work has an owner and a +place, rather than living as an assumption inside somebody else's milestone. + +Named without a `windows-` prefix on purpose: it plans against an abstracted idealized machine and +emits a platform-neutral plan, so nothing in it is Windows-specific. See +[DESIGN-NOTES.md](DESIGN-NOTES.md) -> `EP-D-4` for the architecture, and `EP-D-5` for the layout. + +## What it is + +A **planner**. It takes two inputs and produces a third thing: + +- **a stated goal** -- what the caller intends the arrangement to achieve. Its shape is deliberately + **deferred for litigation**; that is a named deferral, not an omission. +- **an abstracted idealized description of a machine** -- processors, memory, storage, interconnects, + distances and bottlenecks. Not Windows-shaped, and richer than any single platform reports. It is + **mockable by construction**: a description of a machine nobody has is an ordinary input, which is + what makes this component testable without the hardware it plans for. + +From those it produces **a plan**: which processors host domains, where each thread pins, which +memory node each allocates from, what channel connects each pair, and where each channel's buffer +lives. The plan **serializes to JSON** and stays abstracted from Windows. + +**It may ask.** Planning is a negotiation, not a pure function: the component may call back to its +caller through traits for clarifying information the goal did not settle. Which questions those are +is not yet known, and knowing them is what decides whether that is one trait or several. + +## The four components, and which way the arrows point + +| Component | Platform | Depends on | +|---|---|---| +| `topology-model` | neutral | nothing | +| `topology-planner` (this one) | neutral | `topology-model` | +| the inward adapter | Windows | `topology-model`, `windows-topology-sys` | +| the outward adapter (the realizer) | Windows | `topology-model`, the runtime crates | + +`topology-model` holds the abstract machine description, **the traits the planner queries**, and +**the plan type**. Everything depends on it; **nothing depends on this crate**. + +That is the whole point of the arrangement. If the traits lived here, an adapter whose only job is +to describe a machine would have to depend on a planner, and anyone wanting to read a topology would +pull in planning policy they did not ask for. The plan type is here for the same reason one level +down: the realizer *executes* a plan and has no business depending on the policy that chose it. + +## Two kinds of adapter + +**Inward** -- exposes the model's traits over the topology objects already designed, so +`windows_topology_sys::MachineMemoryTopology` becomes one source feeding the abstract model. It is +one source among several: storage and interconnect facts do not come from there, and neither do +measured numbers. + +**Outward (the realizer)** -- takes a plan and **realizes** it in the current process: buffers, +rings and threads, with the user's processing code inserted at the appropriate steps. + +They are separate crates despite both being Windows adapters, because their dependency sets barely +overlap -- the inward one needs only `windows-topology-sys`, while the realizer needs the runtime. +Fusing them would mean anyone reading a topology pulls in the whole runtime. + +## Why the planner is separate from the facts + +Because two different kinds of statement were being made by one crate. + +**`windows-topology-sys` states facts.** Which processors exist, what they share, at what +granularity, how that was established, and what was measured. It never says "use an SPSC ring +here", because that is not a fact about the machine. + +**This crate applies policy.** One domain per core or per thread? Are efficiency cores peers or +excluded? SPSC everywhere, or SPSC within a cache domain and something else across one? Those are +choices, they depend on the workload, and reasonable clients will differ. + +Keeping them in one crate has a specific failure mode, already observed: a policy answer gets +mistaken for a fact and consumers bind to it. `outermost_partitioning_cache` is that -- a single +policy choice ("give me one boundary to shard on") sitting in the facts crate, which three +consumers then re-derived differently. See +[CHECKLIST-ship-topology-and-queues.md](../../CHECKLIST-ship-topology-and-queues.md) SH-16.9. + +## The seam, and how to tell if it is in the right place + +**The planner must not re-derive anything.** If it has to work out for itself which cache level +partitions the machine, or reconstruct a mapping the model already knows, the seam is wrong and the +missing query belongs in `topology-model` -- or, if it is a Windows fact, in the inward adapter. + +That test is the reason this component is being planned *before* the topology model is finished +rather than after: its input requirements are the concrete statement of what the model has to +answer, and they feed the open design session directly. + +## Why it is not the runtime either + +The runtime (M33+, spanning `windows-ioring-sys`, `windows-thread-ambient-sys` and +`windows-namespace-request-sys`) *executes* a plan: it creates the threads, binds them, allocates +the pools, constructs the rings. This crate decides what that plan should be, and the realizer +bridges the two. + +Separating them means a plan is a **value** -- inspectable, comparable, testable against a +synthetic topology for a machine nobody has, and reviewable by a human before anything is pinned +or allocated. A planner fused into the runtime can only be tested by running it on the machine it +plans for, which is exactly the class of test this repository has repeatedly found inadequate. + +The arrangement it plans for is the one +[CHECKLIST-io-domains.md](../../CHECKLIST-io-domains.md) M33+ describes -- "one pinned thread, its +`IoRing`, its node-local registered pool, its shard" -- which is a Seastar-style shard-per-core +runtime. + +## Status and gating + +**Deferred past PR #56, by the engineer's direction.** This component contributes only planning +documents to that PR and no code. The topology reshape it fed requirements into is landing there +without it, because [D-21](../windows-topology-sys/DESIGN-NOTES.md#d-21) establishes that +`windows-topology-sys` publishes a refined view of what the platform publishes and an **adapter** +absorbs whatever this component needs beyond that -- so the reshape is self-justified and the two are +no longer coupled. + +The design session that previously blocked this component has concluded: its questions were answered +as `D-13` through `D-21` in +[windows-topology-sys/DESIGN-NOTES.md](../windows-topology-sys/DESIGN-NOTES.md), and the central +query -- "how close are these two processors?" -- is answered by the ordered relation set that `MMT` +M2 and M4 build. What remains here is this component's own work, not a wait on someone else's. + +The name is settled; the crate does not exist yet. It is deliberately absent from +`release-please-config.json`, the publish workflow's tag patterns, and the workspace manifest until +there is code to publish. diff --git a/crates/topology-planner/DESIGN-NOTES.md b/crates/topology-planner/DESIGN-NOTES.md new file mode 100644 index 000000000..430306c9e --- /dev/null +++ b/crates/topology-planner/DESIGN-NOTES.md @@ -0,0 +1,477 @@ +# Design notes: the topology planner + +Current canonical decisions for this component. See [COMPONENT.md](COMPONENT.md) for what the +component is; see [CHECKLIST.md](CHECKLIST.md) for what is planned. + +`EP-D-1` through `EP-D-3` are **queries** rather than choices: the planner's requirements, stated +precisely enough that the topology model could be designed against a real caller instead of against +a guess. They were written when the planner was to read `windows_topology_sys::MachineMemoryTopology` +directly; [EP-D-4](#ep-d-4) rebinds them to traits over an abstract model, which changes what +satisfies them and not what they require. + +[EP-D-4](#ep-d-4) is the first genuine **choice** here, and it re-scopes the component: the planner +is `topology-planner`, it plans against an abstracted idealized machine, and adapters bracket it on +both sides. [EP-D-5](#ep-d-5) then settles the layout EP-D-4 left open, and the directory has been +renamed to match. + +## Decision index + +| ID | Decision | +|---|---| +| EP-D-1 | **The shard-set query**: what the planner must know to choose which processors host a domain, and what today's model cannot tell it. | +| EP-D-2 | **The proximity query**: how close two processors are, which selects the channel between their domains. Takes an **unordered** pair; the model has no answer today. | +| EP-D-3 | **The residency query**: where a domain's pool lives, and which side of a cross-domain pair should host a shared ring. **Ordered**, and the half the model cannot answer is structurally unanswerable rather than merely unpopulated. | +| EP-D-4 | **The four-part architecture, and the planner's name.** The engineer's position: the planner is **`topology-planner`** (no `windows-` prefix); it takes a **goal** description (shape deferred for litigation), queries an **abstracted idealized** model covering processors, memory, storage, interconnects, distances and bottlenecks, and emits a **JSON-serializable, platform-neutral** plan. Two kinds of **adapter** bracket it: one exposing the planner's traits over the Windows topology objects, one **realizing** a plan as buffers, rings and threads with the user's code inserted at the right steps. Settles `MMT-1.5` (the facts crate keeps its `-sys` name), the "two graphs, one word" ambiguity, and where distance lives -- the attributed interconnect shape D-9 sketched goes in the abstract model, so D-9's deferral in the facts crate stands unreopened. | +| EP-D-5 | **The component layout: `topology-model` is its own crate, and dependencies point one way.** The abstract model and the traits the planner queries live in `topology-model`, which the planner and both adapters depend on; nothing depends on `topology-planner`. Putting the traits in the planner would make a crate whose job is to *describe a machine* depend on one that applies *policy* -- the same defect as `outermost_partitioning_cache`, arriving as a dependency edge instead of an API. Two consequences derived from the same rule rather than decided separately: **the plan type also lives in `topology-model`** (otherwise the realizer depends on the planner), and the inward adapter and the realizer are **separate crates** (their dependency sets barely overlap, and fusing them would make reading a topology pull in the whole runtime). | + +## EP-D-1: the shard-set query + +*Recorded by [CHECKLIST.md](CHECKLIST.md) EP-1.1.* + +### What the planner is choosing + +Which processors may host an execution domain, and how to group them so a policy can pick between +one domain per core and one per logical processor, and can decide whether efficiency cores are +peers, a second tier, or excluded. + +This is the first step of the construction and it fixes the domain count, which everything +downstream is shaped by: the number of rings is quadratic in it, and each domain's memory pool is +sized against it. + +### What it must know, and why + +1. **Identity, as `(group, number)`.** Not a bare index. A processor number without its group names + a different processor in every group and the wrong one in all but the first, and pinning is a + `GROUP_AFFINITY` -- `SetThreadGroupAffinity`, not `SetThreadAffinityMask`, which cannot name + another group at all. A planner that flattens this produces a plan that is silently wrong above + 64 processors. + +2. **Whether the processor is online.** An offline slot exists and counts toward a group's maximum; + planning a domain onto one is planning a thread that cannot run. + +3. **Core membership and whether the core is SMT.** The choice between one domain per core and one + per logical processor is the single largest policy lever, and it needs the sibling grouping, not + just a count. + +4. **Efficiency class.** On a hybrid part, putting latency-sensitive domains on efficiency cores is + a defect the client will not see in a functional test, only in a percentile. + +5. **Whether the processor is available to this process at all** -- parked by the scheduler, or + outside the CPU-set allocation the process was given. + +### What today's model answers + +Points 1 through 3 cleanly. `ProcessorId` is `(group, number)` by construction and documents why +(D-7). `Processor::online` is exactly the distinction in point 2. `DomainKind::Core` carries +`simultaneous_multithreading` and the sibling set, so point 3 is a walk of `MachineMemoryTopology::cores()`. + +Point 4 is answered, but **twice, in two shapes, and one of them is unsafe to use** -- see below. + +Point 5 is **not answered at all**. `GetSystemCpuSetInformation` is consumed nowhere in the +workspace, so `Parked`, `Allocated` and `AllocatedToTargetProcess` are unavailable. A planner +cannot currently avoid pinning a domain to a parked processor, and the client cannot detect that it +happened. Tracked as `SH-16.10` in +[CHECKLIST-ship-topology-and-queues.md](../../CHECKLIST-ship-topology-and-queues.md). + +### `Processor::capacity` must not be used for point 4 + +**Use `DomainKind::Core { efficiency_class, .. }`. Do not use `Processor::capacity`.** + +`capacity` is computed as `online.then(|| find the owning Core domain).flatten().unwrap_or(0)`, so +the value `0` means any of three different things: + +- the processor is offline; +- the processor is online but no `Core` domain names it, which the topology tolerates by design + since firmware coverage is not guaranteed; +- the processor is online, has a core, and its efficiency class genuinely **is** `0`. + +The third is not an edge case. It is **every processor on every non-hybrid machine**, so the +sentinel collides with the overwhelmingly common legitimate value. + +For this planner the collision is worse than for most consumers, because Windows orders efficiency +class with `0` as the *least* performant. On a hybrid part an unknown processor is therefore +indistinguishable from an efficiency core, and a policy that excludes efficiency cores would +silently drop a processor that might be a performance core -- while a policy that tiers them would +place it in the wrong tier. Both failures are invisible in a functional test. + +`Core { efficiency_class }` carries the firmware value with no sentinel, and absence is represented +by the processor being in no `Core` domain, which is a distinguishable state rather than a value. + +**This is the same defect the locality-model session exists to fix, in a third place.** The others: +`ProcessorPlace::cache_domain: Option`, where `None` conflates "no level partitions this +machine" with "this processor was not named at the level that does" (`SH-16.5`); and +`MachineDescription::cpu_model`, where the same conflation was noticed and solved with a side +boolean. Recorded here so the sweep that fixes the model does not stop at the two already known. + +### Partial core coverage is a real state, not a corruption + +A processor in no `Core` domain is a firmware gap, not a contradiction, and the topology crate +tolerates it deliberately. The planner must therefore decide what to do with a processor it cannot +group -- it is a candidate host whose SMT relationships and class are unknown, which is exactly the +"unanswered query" case that [CHECKLIST.md](CHECKLIST.md) EP-1.4 owns. It is named here so that +item is not written as though the case were hypothetical. + +### What this asks of the topology model + +Nothing new in shape; three things in substance. + +- Availability (parked, allocated) has to become expressible, since no policy can be correct + without it. +- Efficiency class has to have exactly one representation, and it must distinguish "class zero" + from "not known". +- Core membership has to admit that a processor may be in no core, without that being an error. + +## EP-D-2: the proximity query + +*Recorded by [CHECKLIST.md](CHECKLIST.md) EP-1.2.* + +### What the planner is choosing + +For two domains, what connects them: a dedicated SPSC ring, a shared MPSC ring fanning several +producers into one consumer, or a routed hop through an intermediate domain. That choice is made +once per pair, and it is made from how close the two processors are. + +This is the query the whole model question turns on. Everything else the planner asks is either +per-processor (EP-D-1) or per-memory-domain (EP-1.3); this is the only one that is *relational*, +and it is the one today's model cannot answer. + +### It takes an unordered pair. The checklist item said ordered, and was wrong. + +`windows-placement-probe` already settled this and stated the reasoning, which is worth quoting +because it is easy to get backwards: + +> These names are deliberately symmetric, and that is not an oversight left over from before hops +> became directed. The *relationship* between two processors genuinely is symmetric -- two +> processors either are SMT siblings or are not, share a cache domain or do not -- so there is no +> honest `CrossNumaNodeForward` to name. Splitting the labels by direction would invent a +> distinction the topology does not have. +> +> The *workload* is what is asymmetric: the producer writes and the consumer reads, so swapping +> them swaps which side pays. Direction therefore lives where it is real, not in the label. + +And on the measured side: "a hop is not symmetric even though the link is." + +So the split is clean, and the planner needs both halves in different places: + +- **Proximity is the link.** Symmetric, unordered pair, answered here. +- **Residency is the hop.** Asymmetric -- which side hosts the ring buffer, which the probe + measures with a dedicated ring-placement column because it was found to matter. That belongs to + EP-1.3, not here. + +Putting direction in the proximity query would invent an asymmetry the topology does not have, and +would double the size of an answer that has no second half to fill. + +### What the answer must contain + +Not a boolean, and not a bare identifier. Three things: + +1. **The tightest granularity the two share.** Comparable against other pairs' answers, because the + policy's threshold ("SPSC within this, MPSC beyond it") is a comparison. The *identity* of the + granularity matters less than its position. + +2. **The membership of that granularity.** Selecting MPSC is not enough -- the planner must size the + fan-in, which is "how many other domains sit at this same proximity". Without membership the + planner would ask the proximity query O(n^2) times and reconstruct the grouping itself, which is + the re-derivation the seam exists to prevent. + +3. **Whether a finer granularity went unobserved.** This is the part that a naive design drops. If + L3 was observed and L2 was not, "tightest shared is L3" is *not* the answer -- the answer is "at + most L3, and finer was not looked at". A planner told the first would choose a slower channel + than the machine can support and never learn why. Under the model's bar -- usable without further + measurement -- it cannot go and check, so the distinction has to be in the answer. + +### The query should be total, which needs a top element + +Two processors in the same machine always share *something*: one address space, one scheduler, one +memory system, however far apart. If the granularity order has no top, the query returns "nothing +in common" for a cross-node pair and every caller writes the same empty-case branch. + +Making "the machine" an explicit top granularity is honest -- it is a real, if loose, locality tier +-- and makes the query total. A bottom ("this processor alone") is the same argument at the other +end and makes `proximity(a, a)` answerable rather than a special case, though a planner has no +reason to ask it. + +### A partial order means the answer may not be a single granularity + +If the order is by observed set inclusion rather than by firmware numbering, two granularities can +be **incomparable** -- neither refines the other. The tightest shared granularity is then not +unique, and the honest answer is the set of *minimal* shared granularities, which is almost always +exactly one. + +This is a cost, and it is worth naming rather than discovering later: every caller either handles a +multi-element answer or documents that it takes the first. But the alternative -- forcing a linear +order -- means silently discarding a real boundary on a machine whose levels do not nest, and this +repository has been bitten specifically by structure that was assumed rather than checked. + +### What today's model answers: nothing + +`MachineMemoryTopology::outermost_partitioning_cache` reports **one level for the whole machine**, and +`Slice::same_cache_domain` reduces that to a boolean at that one level. Neither is pairwise. There +is no query anywhere in `windows-topology-sys` that takes two processors. + +So a planner today reconstructs proximity from the partition list -- which is exactly what +`SH-16.9` records three consumers already doing, in two mutually inconsistent ways. The absence of +this query is the cause of that defect, not a separate problem. + +### What this asks of the model + +- A granularity order derived from **observed set inclusion**, not firmware level numbers, so a + measured-only tier and a machine with no L3 both have positions. +- Access to that order **as a collection**, with a pairwise helper derived from it, returning minimal + shared granularities plus their membership. Stated here first as a pairwise query, which + [windows-topology-sys](../windows-topology-sys/COMPLETED-CHECKLIST.md) `M4+.1` corrected: requiring the answer + to carry the block containing both processors makes it a question about the partition, not the pair, + and a pairwise-primary surface would force the planner into the O(n^2) reconstruction that `SH-16.9` + records going wrong three times. The three *requirements* below are unchanged; only the shape is. +- Unobserved granularities represented, so an answer can be an upper bound and say so. +- A top element, so the query is total. + +## EP-D-3: the residency query + +*Recorded by [CHECKLIST.md](CHECKLIST.md) EP-1.3.* + +### What the planner is choosing + +Two things, and they are different questions that happen to share a subject: + +- **Where each domain's own pool lives.** A domain allocates node-locally to the processor it is + pinned to. Per-processor, unordered, cheap. +- **Which side of a cross-domain pair hosts their shared ring.** Ordered, because the producer + writes and the consumer reads, so the placement decides which of them pays for the crossing. + +This is where the direction that [EP-D-2](#ep-d-2) deliberately refused lands. Proximity is the +link and is symmetric; residency is the hop and is not. + +### The first half is answered, with one asymmetry worth keeping + +`MachineMemoryTopology::memory_domains()` yields the memory domains with their processor sets, so +processor-to-domain is a lookup. + +Partial coverage exists here as it does for caches -- a processor may be named by no memory domain +-- but **the right response is different, and `windows-placement-probe` already got this right**. +Its `places_from_topology` refuses on a missing NUMA node while tolerating a missing cache domain, +and the asymmetry is principled: an unknown cache domain costs an optimisation, whereas an unknown +memory domain has no honest fallback at all, since the pool has to be allocated *somewhere* and +guessing means quietly allocating remote memory for the life of the process. + +So the planner inherits that: an unplaced processor may still host a domain, but not with a +node-local pool, and the difference has to be visible in the plan rather than assumed away. + +### The second half is not merely unpopulated -- it cannot be measured, by construction + +`MachineMemoryTopology::distances` exists, and it is easy to read its permanent `None` as an oversight. It is +not. The field is documented as being for a fed-in description, because "Windows exposes no +user-mode SLIT reader", and that is accurate. + +The sharper problem is what follows from it. `distances` has exactly two input paths: hand +construction, which defaults to `Provenance::Synthetic`, and deserialization, which +`downgraded_to(Provenance::Restored)` caps. `MachineMemoryTopology::discover` hardcodes `None`. So **no path +exists by which `distances` can ever carry `Measured` provenance** -- not because nobody wrote the +code, but because the only sources are a literal and a file, and a file cannot establish that it +describes the machine you are on. + +Under the model's bar -- usable without further measurement -- a planner on a real machine +therefore cannot obtain trustworthy distance for that machine today, and no amount of populating +the existing field would change that. + +### A scalar distance cannot express what this query asks + +Even a populated `Distances` would not answer it. The matrix is SLIT-shaped: one scalar per pair, +with `matrix[i][i]` conventionally `10`. That is a *symmetric, workload-independent* abstraction, +and the residency question is neither. It asks which of two directions is cheaper for a specific +access pattern -- a ring one side writes and the other reads. + +`windows-topology-sys` D-9 already anticipated this precisely, and excluded it deliberately: + +> **HMAT-style attributed relations.** ACPI's Heterogeneous Memory Attribute Table supersedes SLIT, +> giving per-initiator/per-target read and write latency and bandwidth -- four numbers where SLIT +> gives one scalar [...] A general edge list (`{ from, to, read_latency_ns, read_bandwidth_mbps, +> ... }`) would absorb HMAT, **asymmetry**, and multi-hop CXL fabrics; the scalar distance matrix +> this schema keeps will [be revisited when] scalar distance demonstrably mismodels a machine +> somebody is tuning for. + +**This planner is the machine-tuner that deferral names, and asymmetry is exactly the property it +needs.** D-8 makes the revision cheap by keeping the JSON schema outside the semver contract, which +that decision says is "precisely what makes D-9's deferrals safe rather than merely convenient". + +### The trigger is approached, not met, and saying which matters + +D-9's condition is *demonstrable* mismodelling, and honesty requires separating what is shown from +what is expected. + +What is shown: `windows-placement-probe` measures per-hop cost as four numbers per undirected edge +-- two directions times two ring placements -- and its code states that "a hop is not symmetric even +though the link is". The apparatus treats direction as real. + +What is **not** shown: any measurement demonstrating that the four numbers differ. Both development +hosts report a single NUMA node, so every such run is vacuous -- the spike says so itself, printing +"VACUOUS ON THIS MACHINE" and "Apparatus works; question unanswered". So the claim "scalar distance +mismodels this machine" is currently unproven on hardware anyone here has. + +The requirement is real either way, because the planner must choose a side and today has nothing to +choose with. But the *specific* claim that a scalar is insufficient needs a multi-node measurement, +and that measurement should be taken before D-9 is reopened on those grounds rather than after. + +### A measured locality fact must carry what it measured + +The probe's numbers are nanoseconds for one ring-handoff pattern at one message size. Promoting +them into the topology as "the distance" would bake one workload into a model other consumers share +-- and a different consumer, streaming large buffers rather than handing off small messages, would +read them as authoritative and be wrong. + +So a measured relation has to name its measurement, not just its value. This is the concrete reason +per-relation provenance has to be more than a trust label: "measured" is not a sufficient +description of a number whose meaning depends on how it was obtained. + +### What this asks of the model + +- Processor-to-memory-domain, with the unplaced case distinguishable rather than defaulted, because + here it has no honest default. +- A **directed** cost between memory domains, which SLIT's scalar cannot express and which D-9 + already sketched as an attributed edge list. +- Provenance rich enough to say *what* a measured number measured, so one consumer's workload does + not become every consumer's constant. +- And, before reopening D-9 on the asymmetry argument: a multi-node measurement showing the + directions actually differ. + +## EP-D-4: the four-part architecture, and the planner's name + +*The engineer's position, 2026-09-03. This is a **choice**, not one of M1's queries, and it +re-scopes the component that records it.* + +### What was decided + +**The planner is `topology-planner`** -- deliberately with no `windows-` prefix. + +- **Input**: a description of the **goal** of the topology -- what the caller intends the + arrangement to achieve. Its shape is **explicitly deferred for litigation**, which is a named + deferral rather than an omission. +- **What it queries**: an **abstracted, idealized** description of the machine, covering + **processors, memory, storage (NVMe), interconnects, distances, and bottlenecks**. Not + Windows-shaped, and materially richer than what any one platform reports. +- **Output**: a data structure that **serializes to JSON** and is **still abstracted from Windows**. +- **Adapters, in two directions**: + - *inward* -- exposing the traits the planner needs **over the topology objects already designed**, + so `windows_topology_sys::MachineMemoryTopology` becomes one source feeding the abstract model; + - *outward* -- **realizing** a planned topology in the current process as buffers, rings and + threads, with the user's processing code inserted at the appropriate steps. + +### What it settles + +**The crate-naming question** (`MMT-1.5` in +[windows-topology-sys](../windows-topology-sys/COMPLETED-CHECKLIST.md)). The planner does not live in +`windows-topology-sys`, which therefore stays a pure Win32 wrapper and keeps its `-sys` name. The +decisive point is not preference but the adapter boundary: a crate on one side of an adapter is +exactly what `-sys` names, and [D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20) already scoped +that crate to "what the Win32 topology APIs report". + +**"Two graphs, one word"** -- [COMPONENT.md](COMPONENT.md) flagged that both the input and the output +are graphs of processors and relations, so "topology" named all of them and distinguished none. Three +things are now distinct: the **machine memory topology** (Windows facts), the **abstract topology** +(idealized, multi-source, platform-neutral), and the **planned topology** (the output). The word is +shared deliberately; the qualifier carries the distinction. + +**Where distance lives**, which three decisions had left in tension: + +- [D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20) removed `distances` from the facts crate, + because Win32 does not report it and that crate does not go below Win32. +- [EP-D-3](#ep-d-3) established that the planner needs a **directed** cost, which a SLIT-shaped + scalar cannot express. +- `windows-topology-sys` D-9 deferred HMAT-style attributed relations until scalar distance + "demonstrably mismodels a machine somebody is tuning for" -- a trigger this component *approaches* + and, lacking multi-node hardware, has not met. + +The abstract model resolves all three without disturbing any: **interconnects and bottlenecks** are +the attributed-edge shape D-9 sketched, and they live in the abstract model, so D-9's deferral in the +facts crate **stands unreopened** while the need it named is met elsewhere. The measurement condition +still applies before claiming asymmetry is real; it just no longer gates the schema. + +**Storage becomes representable**, which `windows-topology-sys` D-9 also excluded -- on the grounds +that it "changes the crate's identity from processor topology to system topology". That exclusion was +about *that crate* and still holds. NVMe belongs to the abstract model, which was never scoped to a +processor topology. + +### What it opens + +- **Component layout.** How many crates, and where the traits live. If the traits are defined in the + planner, the inward adapter depends on the planner, which points the wrong way for a crate whose + job is to describe a machine. An abstract-model crate that both depend on avoids that, at the cost + of a fourth component. **Not yet decided.** +- **Who measures.** The previous framing had this component measuring with permission. If distance is + a property of the abstract model, measurement plausibly belongs to whatever *populates* that model + -- an adapter -- rather than to the planner. The three-stage split (observe / synthesize / execute) + survives; which component owns the middle stage does not obviously. +- **`MMT-1.3` / `EP-1.4`'s consumer changed.** Both ask what a consumer does with a fact that was not + observed. That consumer is no longer the planner reading `MachineMemoryTopology` directly -- it is + the **inward adapter**, deciding how an absent Windows fact appears in the abstract model. The + decision is still one decision, and it is still to be taken jointly, but it is taken at a boundary + that did not exist when both items were written. + +### What survives unchanged + +[EP-D-1](#ep-d-1), [EP-D-2](#ep-d-2) and [EP-D-3](#ep-d-3) are **requirements**, and requirements +survive a change of binding. Each stated what the planner must know and why; what changes is that +they are now satisfied by traits over an abstract model rather than by methods on a Windows type. +They were written against a real caller, which is what makes them portable in this way. + +## EP-D-5: the component layout, and which way dependencies point + +*The engineer's choice, following [EP-D-4](#ep-d-4). Recorded separately because EP-D-4 explicitly +left it open.* + +### The decision + +**The abstract model and the traits the planner queries live in their own crate, `topology-model`, +which both the planner and the adapters depend on.** + +| Component | Platform | Depends on | +|---|---|---| +| `topology-model` | neutral | nothing | +| `topology-planner` | neutral | `topology-model` | +| inward adapter | Windows | `topology-model`, `windows-topology-sys` | +| outward adapter (realizer) | Windows | `topology-model`, the runtime crates | + +Everything depends on `topology-model`; **nothing depends on `topology-planner`** except a caller +that actually wants to plan. + +### Why not put the traits in the planner + +Because the arrow points the wrong way. An adapter whose job is to describe a machine would have to +depend on a planner in order to describe it, and anyone wanting to read a topology would pull in +planning policy they did not ask for. That is the same defect +[COMPONENT.md](COMPONENT.md) already records in a different place -- `outermost_partitioning_cache`, +a policy answer sitting where facts are stated -- arriving as a dependency edge rather than as an +API. + +### The same rule decides where the plan type goes, one level down + +This is a **derived** consequence rather than a separately-taken decision, and it is called out +because it is easy to miss: the realizer consumes a plan. If the plan type lived in +`topology-planner`, the realizer would depend on the planner -- policy dragged in by a component +whose only job is to execute. + +So **the plan type lives in `topology-model` too**, alongside the machine vocabulary. The crate is +"the shared vocabulary", not merely "the machine description". This is consistent with +[COMPONENT.md](COMPONENT.md)'s existing argument that a plan is a **value** -- inspectable, +comparable, reviewable before anything is pinned or allocated. A value type belongs with the +vocabulary, not with the policy that produced it. + +### Two Windows adapters, not one + +Also derived. They are both Windows adapters and it is tempting to fuse them, but their dependency +sets barely overlap: the inward one needs `windows-topology-sys`, the realizer needs the runtime +(`windows-ioring-sys`, `windows-waitable-queues`, `windows-thread-ambient-sys`). Fusing them would +mean anyone reading a topology pulls in the whole runtime, which is the same "do not drag in what +the caller did not ask for" rule that decided the layout in the first place. + +### What is still open + +- **The adapters' names.** Deliberately not settled here; naming has been getting decided by + whoever writes the first type, and this component has already been renamed once. +- **Who measures.** Carried forward from [EP-D-4](#ep-d-4) and not resolved by the layout: if + distance is a property of the abstract model, measurement plausibly belongs to whatever populates + that model. `topology-model` depends on nothing, so it cannot measure; that puts the measurement in + an adapter or in a fifth thing. +- **Whether `topology-model` is one crate or eventually two.** The machine description and the plan + vocabulary are different enough that they might separate later. They are together now because + splitting on speculation costs more than merging on evidence. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 1fddc78c1..37758b9f2 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -2265,12 +2265,14 @@ well-formed YAML, which nothing currently does. Giving it an owner meant decidin where a workflow-parsing check belongs, which is a repository-level call rather than this crate's, and the root checklist is where that lands. -**This paragraph reads differently on `main`, and deliberately so.** The version -merged from there says the gap is recorded and not yet queued, because `main` has -no `M34` for it to belong to -- the milestone is this branch's. A design note -cannot schedule work, so it has to name the item that does when one exists and -name the absence when one does not; which of those is true depends on the -lineage, and each side says what is true of itself. +**This paragraph used to read differently on `main`, and the difference has since +collapsed.** The `main` version said the gap was recorded and not yet queued, +because `main` had no `M34` for it to belong to. The peel that became PR #95 +moved the root checklist there, so `main` now carries `M34.5` itself and both +copies say the same thing. The lesson survives the divergence it described: a +design note cannot schedule work, so it names the item that does when one exists +and names the absence when one does not -- and which of those is true can change +under it without anything failing. Both are the same lesson this milestone keeps producing: the failure mode of a check is to pass. diff --git a/crates/windows-topology-sys/COMPLETED-CHECKLIST.md b/crates/windows-topology-sys/COMPLETED-CHECKLIST.md index 96cd5e4e3..58bcc1fa6 100644 --- a/crates/windows-topology-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-topology-sys/COMPLETED-CHECKLIST.md @@ -98,7 +98,7 @@ gaps the crate did not have and were closed as already-satisfied or re-planned r implemented; the notes below record which, and why. Design record: [DESIGN-NOTES.md](DESIGN-NOTES.md) `D-13` through `D-23`, and -DESIGN-SESSION-2026-09-02-cache-locality-model.md. +[DESIGN-SESSION-2026-09-02-cache-locality-model.md](../../design-sessions/DESIGN-SESSION-2026-09-02-cache-locality-model.md). The context the milestones were written against, preserved because the items refer to it: ## What this is for @@ -213,7 +213,7 @@ be wrong and breaking again later. `discover()` returns a topology stale the instant it returns, so the two-call window is only a larger instance of an unavoidable problem. True, and **not a reason to do nothing**: the two are not equally addressable. Staleness after the fact is the executor's to validate, and is already - owned as `M-inf.1` in topology-planner. + owned as `M-inf.1` in [topology-planner](../topology-planner/CHECKLIST.md). Incoherence *during* collection is ours, detectable, and cheap to fix. The framing is what caused the miss. Asking "what do we **store** when sources disagree" admits @@ -248,9 +248,9 @@ be wrong and breaking again later. far the parts may be **correlated** -- a different question from whether any one part is accurate. Turning that into something actionable, with the identifying provenance an actionable report needs, is the probe tools' job and is tracked as **M7** in - CHECKLIST-placement-tool.md. + [CHECKLIST-placement-tool.md](../../CHECKLIST-placement-tool.md). > **-> CROSS-COMPONENT HANDOFF:** the reporting half is `PT-7.1` and `PT-7.2` in - > CHECKLIST-placement-tool.md. That tool already carries the + > [CHECKLIST-placement-tool.md](../../CHECKLIST-placement-tool.md). That tool already carries the > review this needs -- the runner sees real values before sending (`PT-4.5`), the README lists what > is collected (`PT-4.3`), and suppression is recorded rather than merely absent. Only possible because [D-15](DESIGN-NOTES.md#d-15) keeps both observations: a disagreement cannot @@ -320,7 +320,7 @@ be wrong and breaking again later. landed in a "decisions that shape everything below" milestone because it *looked* foundational, and foundational-looking is not the same as being about this component. > **-> CROSS-COMPONENT HANDOFF:** the behavioural half is `EP-1.4` in - > topology-planner. It no longer has a counterpart here, so it + > [topology-planner](../topology-planner/CHECKLIST.md). It no longer has a counterpart here, so it > is that component's decision alone rather than a joint one. - [x] **MMT-1.4** -- **Does `distances` survive at all?** The two-component architecture says the @@ -339,12 +339,12 @@ be wrong and breaking again later. - [x] **MMT-1.5** -- **Does the synthesizer live in this crate, and therefore what is this crate called?** Recorded as open rather than settled: see - topology-planner/COMPONENT.md. The naming follows + [topology-planner/COMPONENT.md](../topology-planner/COMPONENT.md). The naming follows the merge rather than leading it -- while this crate is only a Win32 wrapper, `-sys` is correct for it; if it gains a synthesizer that measures, it stops being one and the name should change then. **Answered by the engineer's architectural shift, recorded as - EP-D-4: no.** The planner is a separate + [EP-D-4](../topology-planner/DESIGN-NOTES.md#ep-d-4): no.** The planner is a separate component named **`topology-planner`** -- with no `windows-` prefix, because it plans against an abstracted idealized machine and emits a platform-neutral plan. So this crate does not gain the synthesizer, remains a pure Win32 wrapper, and **keeps its name**. @@ -627,12 +627,12 @@ facts about processors and memory stated without sentinels; `M4+.4` fixes a rule restated three times in two crates. Only `M4+.1`'s pairwise helper is consumer-flavoured, and the ordered collection it derives from is what stops that restatement recurring. -They remain cross-referenced to topology-planner as +They remain cross-referenced to [topology-planner](../topology-planner/DESIGN-NOTES.md) as **evidence** the shape is right rather than as its justification -- stating those requirements found the `Processor::capacity` sentinel collision that reviewing the model alone had not. - [x] **M4+.1** -- **The ordered relations are the query surface; pairwise proximity is a method on - them.** The requirement arrived from EP-D-2 as a + them.** The requirement arrived from [EP-D-2](../topology-planner/DESIGN-NOTES.md#ep-d-2) as a *pairwise* query returning the minimal shared granularities, **their membership**, and whether a finer granularity went **unobserved** so the answer can be an upper bound and say so. All three requirements stand. The **shape** does not, and the requirement says so itself: it asks the answer diff --git a/crates/windows-topology-sys/src/granularity.rs b/crates/windows-topology-sys/src/granularity.rs index 2b6b9ff0d..93e8bed5b 100644 --- a/crates/windows-topology-sys/src/granularity.rs +++ b/crates/windows-topology-sys/src/granularity.rs @@ -317,12 +317,13 @@ pub struct Proximity<'a> { /// processors appears in *no* instance of, so the platform has said nothing /// about whether they share it. /// - /// This is the third of `EP-D-2`'s requirements, and the one a naive design + /// This is the third of [EP-D-2]'s requirements, and the one a naive design /// drops. A caller told "the tightest shared thing is L3" when in truth L2 /// was never reported for this processor would choose a slower channel than /// the machine can support and never learn why -- and under this crate's own /// bar it cannot go and measure to find out. /// + /// [EP-D-2]: ../../topology-planner/DESIGN-NOTES.md#ep-d-2 pub finer_unobserved: bool, /// Processors the query named that this platform cannot express, and which /// therefore took no part in [`Self::shared`].