From 867cb20ed4eb299165a61a97faf4a2180f0e4248 Mon Sep 17 00:00:00 2001 From: llols Date: Thu, 5 Nov 2026 20:57:00 +0000 Subject: [PATCH] Preserve fixed native trace assignments across rerouting --- README.md | 8 + lib/bus-solver/TinyHyperGraphBusSolver.ts | 3 + lib/compat/convertToSerializedHyperGraph.ts | 1 + lib/compat/loadSerializedHyperGraph.ts | 5 + lib/core.ts | 17 ++- lib/initialAssignments.ts | 18 ++- ...e-in-partial-rip-tiny-hypergraph-solver.ts | 1 + ...GraphSolverWithStableInitialAssignments.ts | 8 +- lib/region-graph/graph.ts | 3 + .../TinyHyperGraphSectionPipelineSolver.ts | 2 + lib/section-solver/index.ts | 11 +- ...selective-rerip-tiny-hyper-graph-solver.ts | 17 ++- ...partial-rip-tiny-hypergraph-solver.test.ts | 12 ++ ...lective-rerip-blocker-reachability.test.ts | 9 ++ .../solver/fixed-initial-assignments.test.ts | 142 ++++++++++++++++++ 15 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 tests/solver/fixed-initial-assignments.test.ts diff --git a/README.md b/README.md index b9e423e8..e4ed5709 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ existing ports and contribute to region congestion immediately, but remain eligible for the normal rip-and-reroute process. They do not create regions or otherwise change the hypergraph topology. +Set `isFixed: true` on a serialized connection to preserve its assignments +through global retries, selective and partial rerips, and section optimization. +The loader stores this policy in `problem.routeIsFixed`, and serialized output +preserves it. A fixed route requires complete initial assignments connecting its +endpoints. Its ports and segments remain native routing state, so other routes +on the same net can reuse them. Omitted `isFixed` keeps the normal mutable policy. +The independent bus and region-path solvers reject fixed routes. + ### Outside-in partial reripping `SelectiveReripTinyHyperGraphSolver` preserves the unaffected prefix and diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 557934c5..9cd9a364 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -114,6 +114,9 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { problem: TinyHyperGraphProblem, options?: TinyHyperGraphBusSolverOptions, ) { + if (problem.routeIsFixed?.some((isFixed) => isFixed === 1)) { + throw new Error("TinyHyperGraphBusSolver does not support fixed routes") + } super(topology, problem, options) this.busTraceOrder = deriveBusTraceOrder(topology, problem) diff --git a/lib/compat/convertToSerializedHyperGraph.ts b/lib/compat/convertToSerializedHyperGraph.ts index 0d2e2223..eeb40a70 100644 --- a/lib/compat/convertToSerializedHyperGraph.ts +++ b/lib/compat/convertToSerializedHyperGraph.ts @@ -314,6 +314,7 @@ const getSerializedConnection = ( endRegionId: metadataEndRegionId ?? endRegionId, mutuallyConnectedNetworkId: metadataNetworkId ?? `net-${solver.problem.routeNet[routeId]}`, + ...(solver.problem.routeIsFixed?.[routeId] === 1 && { isFixed: true }), } } diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 63fbdfdd..afcafc79 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -508,6 +508,7 @@ export const loadSerializedHyperGraph = ( const routeStartPort = new Int32Array(routeCount) const routeEndPort = new Int32Array(routeCount) const routeNet = new Int32Array(routeCount) + const routeIsFixed = new Uint8Array(routeCount) routableConnections.forEach(({ connection, solvedRoute }, routeIndex) => { const fallbackStartPortId = getCentermostPortIdForRegion( @@ -543,6 +544,9 @@ export const loadSerializedHyperGraph = ( routeEndPort[routeIndex] = endPortIndex routeNet[routeIndex] = getNetIndex(connection) + if ("isFixed" in connection && connection.isFixed === true) { + routeIsFixed[routeIndex] = 1 + } }) const topology: TinyHyperGraphTopology = { @@ -612,6 +616,7 @@ export const loadSerializedHyperGraph = ( routeEndPort, routeNet, regionNetId, + ...(routeIsFixed.some((isFixed) => isFixed === 1) && { routeIsFixed }), ...(initialAssignments.length > 0 && { initialAssignments }), ...(hasPortPenalties && { portPenalty }), } diff --git a/lib/core.ts b/lib/core.ts index 8bd0e56d..b596ec00 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -144,6 +144,8 @@ export interface TinyHyperGraphProblem { // routeNet[routeId] = net id of the route routeNet: Int32Array // NetId[] + /** routeIsFixed[routeId] = 1 preserves the route's initial assignments during every rerip. */ + routeIsFixed?: Uint8Array /** regionNetId[regionId] = reserved net id for the region, -1 means freely traversable */ regionNetId: Int32Array @@ -154,7 +156,7 @@ export interface TinyHyperGraphProblem { * Existing serialized region assignments, converted to numeric ids. * * These are regular route-owned assignments: they seed the initial routing - * state and may be ripped and rerouted by the normal solver machinery. + * state and may be ripped and rerouted unless routeIsFixed marks them fixed. */ initialAssignments?: TinyHyperGraphInitialAssignment[] } @@ -1452,6 +1454,19 @@ export class TinyHyperGraphSolver extends BaseSolver { state.candidateQueue.clear() this.resetCandidateBestCosts() state.goalPortId = -1 + applyInitialAssignments({ + topology, + problem: { + ...problem, + initialAssignments: problem.initialAssignments?.filter( + ({ routeId }) => problem.routeIsFixed?.[routeId] === 1, + ), + }, + state, + routeSuccessCountByRouteId: this.routeSuccessCountByRouteId, + appendSegmentToRegionCache: (regionId, fromPortId, toPortId) => + this.appendSegmentToRegionCache(regionId, fromPortId, toPortId), + }) } protected getMaxRegionCost() { diff --git a/lib/initialAssignments.ts b/lib/initialAssignments.ts index a907fb92..551d135e 100644 --- a/lib/initialAssignments.ts +++ b/lib/initialAssignments.ts @@ -18,6 +18,7 @@ interface InitialAssignmentProblem { routeStartPort: Int32Array routeEndPort: Int32Array routeNet: Int32Array + routeIsFixed?: Uint8Array initialAssignments?: TinyHyperGraphInitialAssignment[] } @@ -98,7 +99,12 @@ export const applyInitialAssignments = ({ } | undefined => { const assignments = problem.initialAssignments ?? [] - if (assignments.length === 0) return + if ( + problem.routeIsFixed !== undefined && + problem.routeIsFixed.length !== problem.routeCount + ) { + throw new Error("routeIsFixed must contain one entry per route") + } const assignmentsByRoute = new Map< RouteId, @@ -147,6 +153,16 @@ export const applyInitialAssignments = ({ assertAssignmentsConnectRoute(problem, routeId, routeAssignments) } + for (let routeId = 0; routeId < problem.routeCount; routeId++) { + if ( + problem.routeIsFixed?.[routeId] === 1 && + !assignmentsByRoute.has(routeId) + ) { + throw new Error(`Fixed route ${routeId} requires initial assignments`) + } + } + if (assignments.length === 0) return + const initiallyRoutedRouteIds = new Set(assignmentsByRoute.keys()) for (const { routeId, regionId, fromPortId, toPortId } of assignments) { const routeNetId = problem.routeNet[routeId]! diff --git a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts index be894922..72bf1ee5 100644 --- a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts +++ b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts @@ -452,6 +452,7 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy if (!orderedSegments) return false if ( + this.problem.routeIsFixed?.[routeId] === 1 || !routeIdsTouchingHotRegions.has(routeId) || (hasNonPreferredRouteTouchingHotRegions && preferredPreservedRouteIds.has(routeId)) diff --git a/lib/prepared/SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments.ts b/lib/prepared/SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments.ts index d92eda30..da42f2a1 100644 --- a/lib/prepared/SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments.ts +++ b/lib/prepared/SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments.ts @@ -26,7 +26,13 @@ export class SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments exte applyInitialAssignments({ topology: this.topology, - problem: this.problem, + problem: { + ...this.problem, + routeIsFixed: undefined, + initialAssignments: this.problem.initialAssignments.filter( + ({ routeId }) => this.problem.routeIsFixed?.[routeId] !== 1, + ), + }, state: this.state, routeSuccessCountByRouteId: this.routeSuccessCountByRouteId, appendSegmentToRegionCache: (regionId, fromPortId, toPortId) => diff --git a/lib/region-graph/graph.ts b/lib/region-graph/graph.ts index bc4d2a10..f5890620 100644 --- a/lib/region-graph/graph.ts +++ b/lib/region-graph/graph.ts @@ -180,6 +180,9 @@ export const createRegionPathProblem = ( topology: TinyHyperGraphTopology, problem: TinyHyperGraphProblem, ): RegionPathProblem => { + if (problem.routeIsFixed?.some((isFixed) => isFixed === 1)) { + throw new Error("RegionPathSolver does not support fixed routes") + } const regionIndexBySerializedId = getRegionIndexBySerializedId(topology) const routeStartRegion = new Int32Array(problem.routeCount) const routeEndRegion = new Int32Array(problem.routeCount) diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index 9ccd4f97..1f9140d2 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -122,6 +122,8 @@ const createProblemWithPortSectionMask = ( routeStartPort: new Int32Array(problem.routeStartPort), routeEndPort: new Int32Array(problem.routeEndPort), routeNet: new Int32Array(problem.routeNet), + routeIsFixed: problem.routeIsFixed, + initialAssignments: problem.initialAssignments, regionNetId: new Int32Array(problem.regionNetId), portPenalty: problem.portPenalty === undefined diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index 2f4bd0e2..926b864a 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -501,7 +501,7 @@ const createSectionRoutePlans = ( }) } - if (maskedRuns.length === 0) { + if (problem.routeIsFixed?.[routeId] === 1 || maskedRuns.length === 0) { for (let portIndex = 1; portIndex < orderedPortIds.length; portIndex++) { routePlan.fixedSegments.push({ regionId: orderedRegionIds[portIndex - 1]!, @@ -565,6 +565,14 @@ const createSectionRoutePlans = ( routeStartPort, routeEndPort, routeNet: new Int32Array(problem.routeNet), + routeIsFixed: problem.routeIsFixed, + initialAssignments: routePlans.flatMap((routePlan) => { + if (problem.routeIsFixed?.[routePlan.routeId] !== 1) return [] + return routePlan.fixedSegments.map((segment) => ({ + routeId: routePlan.routeId, + ...segment, + })) + }), regionNetId: new Int32Array(problem.regionNetId), portPenalty: problem.portPenalty === undefined @@ -636,6 +644,7 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { applyFixedSegments() { for (const routePlan of this.routePlans) { + if (this.problem.routeIsFixed?.[routePlan.routeId] === 1) continue for (const { regionId, fromPortId, diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index a298599d..078c063d 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -380,6 +380,16 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH ) } + const forbiddenOwners = new Set(forbiddenOwnerRouteIds) + for ( + let ownerRouteId = 0; + ownerRouteId < this.problem.routeCount; + ownerRouteId++ + ) { + if (this.problem.routeIsFixed?.[ownerRouteId] === 1) { + forbiddenOwners.add(ownerRouteId) + } + } const portOwners = this.getPortOwners() const boundedReachabilityGeometry = this.hasBoundedReachabilityGeometry({ startPortId, @@ -399,7 +409,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH portOwners, portResources, hopTemplatesByRegion, - forbiddenOwnerRouteIds, + forbiddenOwnerRouteIds: forbiddenOwners, } const maxExpandedLabels = this.getRelaxedSearchExpansionLimit() const searchOptions: DistinctOwnerBlockerSearchOptions< @@ -805,6 +815,11 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH } private rebuildCommittedState(rippedRouteIds: ReadonlySet): void { + for (const routeId of rippedRouteIds) { + if (this.problem.routeIsFixed?.[routeId] === 1) { + throw new Error(`Cannot rip fixed route ${routeId}`) + } + } const previousRegionSegments = this.state.regionSegments this.state.regionSegments = this.state.regionSegments.map((segments) => segments.filter(([routeId]) => !rippedRouteIds.has(routeId)), diff --git a/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts b/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts index 7e683bca..8c6f58b1 100644 --- a/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts +++ b/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts @@ -118,6 +118,18 @@ test("partial rip leaves preferred routes unchanged when another hot route can m ]) }) +test("partial rip keeps fixed routes even when no movable hot route remains", () => { + const solver = createLinearSolver() + solver.problem.routeIsFixed = new Uint8Array([1]) + const initialRegionSegments = structuredClone(solver.state.regionSegments) + const regionCosts = new Float64Array(6) + regionCosts[3] = 1 + + expect(solver.prepare([3], regionCosts)).toBe(false) + expect(solver.state.regionSegments).toEqual(initialRegionSegments) + expect(solver.state.unroutedRoutes).toEqual([]) +}) + test("partial rip locally rerips a preferred route when every hot route is preferred", () => { const solver = createLinearSolver() solver.preferredPreservedRouteIds.add(0) diff --git a/tests/selective-rerip-blocker-reachability.test.ts b/tests/selective-rerip-blocker-reachability.test.ts index 9fbbbc8d..a996ad36 100644 --- a/tests/selective-rerip-blocker-reachability.test.ts +++ b/tests/selective-rerip-blocker-reachability.test.ts @@ -62,6 +62,15 @@ test("selective search rejects exact owner cuts and keeps preservation searches }) expect(solver.searchPreferringPreservedRoutes()).toEqual(direct) + problem.routeIsFixed = new Uint8Array([0, 1]) + expect(solver.search()).toEqual({ + found: false, + reason: "no_path", + expandedLabelCount: 1, + }) + expect(solver.searchPreferringPreservedRoutes()).toEqual(solver.search()) + problem.routeIsFixed = undefined + problem.regionNetId[1] = 2 expect(solver.search()).toEqual({ found: false, diff --git a/tests/solver/fixed-initial-assignments.test.ts b/tests/solver/fixed-initial-assignments.test.ts new file mode 100644 index 00000000..768d0b17 --- /dev/null +++ b/tests/solver/fixed-initial-assignments.test.ts @@ -0,0 +1,142 @@ +import { expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { gunzipSync } from "node:zlib" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { + loadSerializedHyperGraph, + PreparedPortPointRoutingSolver, + RegionPathSolver, + TinyHyperGraphBusSolver, + TinyHyperGraphSectionSolver, + TinyHyperGraphSolver, +} from "lib/index" +import type { PortId } from "lib/types" + +const loadFixedT113Board = () => { + const fixture = JSON.parse( + gunzipSync( + new Uint8Array( + readFileSync( + new URL( + "../fixtures/t113-route201-real-repro.json.gz", + import.meta.url, + ), + ), + ), + ).toString("utf8"), + ) as { serializedHyperGraph: SerializedHyperGraph } + const graph = fixture.serializedHyperGraph + const preloadedConnectionIds = new Set( + graph.regions.flatMap((region) => + (region.assignments ?? []).map((assignment) => assignment.connectionId), + ), + ) + graph.connections = graph.connections?.map((connection) => { + if (!preloadedConnectionIds.has(connection.connectionId)) return connection + return { ...connection, isFixed: true } + }) + return loadSerializedHyperGraph(graph) +} + +test("fixed T113 fanout traces survive global retries and allow native same-net reuse", () => { + const input = loadFixedT113Board() + const solver = new PreparedPortPointRoutingSolver( + input.topology, + input.problem, + { RIP_THRESHOLD_RAMP_ATTEMPTS: 0, MAX_ITERATIONS: 200_000 }, + ) + const initialRegionSegments = structuredClone(solver.state.regionSegments) + const initialPortAssignment = new Int32Array(solver.state.portAssignment) + + expect( + input.problem.routeIsFixed?.filter((isFixed) => isFixed === 1), + ).toHaveLength(192) + expect(solver.state.unroutedRoutes).toHaveLength(42) + for (let retry = 0; retry < 2; retry++) { + solver.resetRoutingStateForRerip() + expect(solver.state.regionSegments).toEqual(initialRegionSegments) + expect(solver.state.portAssignment).toEqual(initialPortAssignment) + expect(solver.state.unroutedRoutes).toHaveLength(42) + expect( + solver.state.regionIntersectionCaches.reduce( + (segmentCount, cache) => segmentCount + cache.existingSegmentCount, + 0, + ), + ).toBe(644) + } + + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect( + solver.state.regionSegments.map((segments) => + segments.filter( + ([routeId]) => input.problem.routeIsFixed?.[routeId] === 1, + ), + ), + ).toEqual(initialRegionSegments) + + const reusedPorts = new Set() + for (const [ + routeId, + fromPortId, + toPortId, + ] of solver.state.regionSegments.flat()) { + if (input.problem.routeIsFixed?.[routeId] === 1) continue + for (const portId of [fromPortId, toPortId]) { + if (initialPortAssignment[portId] === -1) continue + expect(input.problem.routeNet[routeId]).toBe( + initialPortAssignment[portId], + ) + reusedPorts.add(portId) + } + } + expect(reusedPorts.size).toBeGreaterThan(0) + + const replay = loadSerializedHyperGraph(solver.getOutput()) + expect(replay.problem.routeIsFixed).toEqual(input.problem.routeIsFixed) + const sectionSolver = new TinyHyperGraphSectionSolver( + replay.topology, + replay.problem, + replay.solution, + { MAX_RIPS: 0, MAX_ITERATIONS: 200_000 }, + ) + sectionSolver.solve() + expect(sectionSolver.solved).toBe(true) + expect(sectionSolver.activeRouteIds).toHaveLength(42) + expect( + sectionSolver.activeRouteIds.every( + (routeId) => replay.problem.routeIsFixed?.[routeId] !== 1, + ), + ).toBe(true) + expect( + sectionSolver + .getSolvedSolver() + .state.regionSegments.map((segments) => + segments.filter( + ([routeId]) => replay.problem.routeIsFixed?.[routeId] === 1, + ), + ), + ).toEqual(initialRegionSegments) +}) + +test("base solver restores fixed T113 assignments and rejects an unseeded fixed route", () => { + const input = loadFixedT113Board() + const solver = new TinyHyperGraphSolver(input.topology, input.problem) + const initialRegionSegments = structuredClone(solver.state.regionSegments) + solver.resetRoutingStateForRerip() + expect(solver.state.regionSegments).toEqual(initialRegionSegments) + expect( + () => new TinyHyperGraphBusSolver(input.topology, input.problem), + ).toThrow("TinyHyperGraphBusSolver does not support fixed routes") + expect(() => new RegionPathSolver(input.topology, input.problem)).toThrow( + "RegionPathSolver does not support fixed routes", + ) + const missingRouteId = input.problem.initialAssignments![0]!.routeId + input.problem.initialAssignments = input.problem.initialAssignments?.filter( + ({ routeId }) => routeId !== missingRouteId, + ) + expect(() => new TinyHyperGraphSolver(input.topology, input.problem)).toThrow( + `Fixed route ${missingRouteId} requires initial assignments`, + ) +})