diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 5100584..aba9066 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -16,6 +16,7 @@ export const computeRegionCost = ( traceCount: number, regionAvailableZMask = 0, minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER, + traceDensityCostFactor = 0, ) => { const area = regionWidth * regionHeight @@ -27,6 +28,7 @@ export const computeRegionCost = ( traceCount, regionAvailableZMask, minViaPadDiameter, + traceDensityCostFactor, ) } @@ -38,6 +40,7 @@ export const computeRegionCostForArea = ( traceCount: number, regionAvailableZMask = 0, minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER, + traceDensityCostFactor = 0, ) => { const estViasRequired = numSameLayerIntersections * 2 + @@ -52,9 +55,28 @@ export const computeRegionCostForArea = ( ) ? numSameLayerIntersections * IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST : 0 + const layerCount = countAvailableLayers(regionAvailableZMask) + const traceDensityCost = + (traceDensityCostFactor * + (traceCount / layerCount) ** 2 * + traceWidth ** 2) / + area return ( (estViasRequired * viaSizeWithMarginSq * traceCountMult) / area + - impossibleSingleLayerIntersectionCost + impossibleSingleLayerIntersectionCost + + traceDensityCost ) } + +const countAvailableLayers = (regionAvailableZMask: number) => { + if (regionAvailableZMask === 0) return 2 + + let mask = regionAvailableZMask >>> 0 + let count = 0 + while (mask !== 0) { + count += mask & 1 + mask >>>= 1 + } + return count +} diff --git a/lib/core.ts b/lib/core.ts index 7a93123..4c6cc93 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -255,6 +255,8 @@ export interface TinyHyperGraphSolverOptions { RIP_THRESHOLD_END?: number RIP_THRESHOLD_RAMP_ATTEMPTS?: number RIP_CONGESTION_REGION_COST_FACTOR?: number + /** Opt-in quadratic penalty for concentrating traces in low-capacity regions. */ + TRACE_DENSITY_COST_FACTOR?: number USE_LAZY_ROUTE_HEURISTIC?: boolean USE_SPARSE_CANDIDATE_STORAGE?: boolean MAX_ITERATIONS?: number @@ -301,6 +303,7 @@ export interface TinyHyperGraphSolverOptionTarget { RIP_THRESHOLD_END: number RIP_THRESHOLD_RAMP_ATTEMPTS: number RIP_CONGESTION_REGION_COST_FACTOR: number + TRACE_DENSITY_COST_FACTOR?: number USE_LAZY_ROUTE_HEURISTIC?: boolean USE_SPARSE_CANDIDATE_STORAGE?: boolean MAX_ITERATIONS: number @@ -351,6 +354,12 @@ export const applyTinyHyperGraphSolverOptions = ( solver.RIP_CONGESTION_REGION_COST_FACTOR = options.RIP_CONGESTION_REGION_COST_FACTOR } + if (options.TRACE_DENSITY_COST_FACTOR !== undefined) { + solver.TRACE_DENSITY_COST_FACTOR = Math.max( + 0, + options.TRACE_DENSITY_COST_FACTOR, + ) + } if (options.USE_LAZY_ROUTE_HEURISTIC !== undefined) { solver.USE_LAZY_ROUTE_HEURISTIC = options.USE_LAZY_ROUTE_HEURISTIC } @@ -433,6 +442,7 @@ export const getTinyHyperGraphSolverOptions = ( RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, RIP_THRESHOLD_RAMP_ATTEMPTS: solver.RIP_THRESHOLD_RAMP_ATTEMPTS, RIP_CONGESTION_REGION_COST_FACTOR: solver.RIP_CONGESTION_REGION_COST_FACTOR, + TRACE_DENSITY_COST_FACTOR: solver.TRACE_DENSITY_COST_FACTOR, USE_LAZY_ROUTE_HEURISTIC: solver.USE_LAZY_ROUTE_HEURISTIC, USE_SPARSE_CANDIDATE_STORAGE: solver.USE_SPARSE_CANDIDATE_STORAGE, MAX_ITERATIONS: solver.MAX_ITERATIONS, @@ -505,6 +515,7 @@ export class TinyHyperGraphSolver extends BaseSolver { RIP_THRESHOLD_RAMP_ATTEMPTS = 50 RIP_CONGESTION_REGION_COST_FACTOR = 0.1 + TRACE_DENSITY_COST_FACTOR = 0 USE_LAZY_ROUTE_HEURISTIC = false USE_SPARSE_CANDIDATE_STORAGE = false @@ -965,6 +976,7 @@ export class TinyHyperGraphSolver extends BaseSolver { traceCount, this.topology.regionAvailableZMask?.[regionId] ?? 0, this.minViaPadDiameter, + this.TRACE_DENSITY_COST_FACTOR, ) } diff --git a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts index da977e9..be89492 100644 --- a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts +++ b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts @@ -65,6 +65,8 @@ type CompletedRoundSummary = RegionCostSummary & { squaredRegionSegmentCount: number } +const NO_PREFERRED_PRESERVED_ROUTE_IDS = new Set() + /** * Retains the two outside portions of a completed route and only reroutes a * bounded window around a congested region. The active window is represented @@ -234,6 +236,14 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy } } + /** + * Quality rerips retain these routes when other routes cross the same hot + * regions. Completion rerips may still use them when they are unavoidable. + */ + protected getRouteIdsPreferredForPreservation(): ReadonlySet { + return NO_PREFERRED_PRESERVED_ROUTE_IDS + } + private getCommittedRouteSegments( routeId: RouteId, ): CommittedRouteSegment[] | undefined { @@ -423,6 +433,11 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy } } if (routeIdsTouchingHotRegions.size === 0) return false + const preferredPreservedRouteIds = + this.getRouteIdsPreferredForPreservation() + const hasNonPreferredRouteTouchingHotRegions = [ + ...routeIdsTouchingHotRegions, + ].some((routeId) => !preferredPreservedRouteIds.has(routeId)) const retainedSegmentsByRegion = Array.from( { length: this.topology.regionCount }, @@ -436,7 +451,11 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy const orderedSegments = this.getCommittedRouteSegments(routeId) if (!orderedSegments) return false - if (!routeIdsTouchingHotRegions.has(routeId)) { + if ( + !routeIdsTouchingHotRegions.has(routeId) || + (hasNonPreferredRouteTouchingHotRegions && + preferredPreservedRouteIds.has(routeId)) + ) { for (const segment of orderedSegments) { this.appendRetainedSegment(retainedSegmentsByRegion, routeId, segment) retainedSegmentCount += 1 diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index a297c00..bb93ff6 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -50,7 +50,11 @@ export type SelectiveReripTinyHyperGraphStats = { selectiveRipCount: number selectivelyRippedRouteCount: number globalReripCount: number - globalReripReason?: "no_path" | "expansion_limit" | "no_blocker_path" + globalReripReason?: + | "no_path" + | "expansion_limit" + | "no_blocker_path" + | "failed_owner_cycle" alternateBlockerSearchCount: number alternateOwnerCount: number failedOwnerPairCount: number @@ -193,7 +197,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH ) } - const directPath = this.findRelaxedBlockerPath() + const directPath = this.findRelaxedBlockerPathPreferringPreservedRoutes() if (!directPath.found || directPath.owners.size === 0) { this.selectiveReripStats.globalReripCount += 1 this.selectiveReripStats.globalReripReason = !directPath.found @@ -218,6 +222,26 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH const count = this.incrementFailedOwnerPair(failedRouteId, ownerRouteId) if (count >= 2) repeatedOwnerRouteIds.push(ownerRouteId) } + if ( + directOwnerRouteIds.some((ownerRouteId) => + this.hasFailedOwnerPath(ownerRouteId, failedRouteId), + ) + ) { + this.selectiveReripStats.globalReripCount += 1 + this.selectiveReripStats.globalReripReason = "failed_owner_cycle" + this.selectiveReripStats.lastFailedRouteId = failedRouteId + this.selectiveReripStats.lastDirectOwnerRouteIds = directOwnerRouteIds + this.selectiveReripStats.lastRepeatedOwnerRouteIds = repeatedOwnerRouteIds + this.selectiveReripStats.lastAlternateOwnerRouteIds = [] + this.selectiveReripStats.lastRippedRouteIds = [] + this.selectiveReripStats.lastRelaxedSearchExpandedLabelCount = + directPath.expandedLabelCount + this.selectiveReripStats.lastAlternateSearchExpandedLabelCount = 0 + this.failedOwnerPairCounts.clear() + super.onOutOfCandidates() + this.publishSelectiveReripStats() + return + } let alternatePath: | DistinctOwnerBlockerSearchResult< @@ -228,7 +252,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH | undefined if (repeatedOwnerRouteIds.length > 0) { this.selectiveReripStats.alternateBlockerSearchCount += 1 - alternatePath = this.findRelaxedBlockerPath( + alternatePath = this.findRelaxedBlockerPathPreferringPreservedRoutes( new Set(repeatedOwnerRouteIds), ) if (!alternatePath.found) { @@ -350,6 +374,33 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH }) } + protected findRelaxedBlockerPathPreferringPreservedRoutes( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ): DistinctOwnerBlockerSearchResult< + RelaxedSearchState, + RouteId, + RelaxedSearchHopData + > { + const preferredPreservedRouteIds = + this.getRouteIdsPreferredForPreservation() + if (preferredPreservedRouteIds.size === 0) { + return this.findRelaxedBlockerPath(forbiddenOwnerRouteIds) + } + + const preferredForbiddenOwnerRouteIds = new Set(forbiddenOwnerRouteIds) + for (const routeId of preferredPreservedRouteIds) { + preferredForbiddenOwnerRouteIds.add(routeId) + } + const preferredPath = this.findRelaxedBlockerPath( + preferredForbiddenOwnerRouteIds, + ) + if (preferredPath.found && preferredPath.owners.size > 0) { + return preferredPath + } + + return this.findRelaxedBlockerPath(forbiddenOwnerRouteIds) + } + protected getRelaxedSearchExpansionLimit(): number { let incidentHopCount = 0 for (const incidentRegions of this.topology.incidentPortRegion) { @@ -620,6 +671,24 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH return count } + private hasFailedOwnerPath( + fromRouteId: RouteId, + targetRouteId: RouteId, + ): boolean { + const pendingRouteIds = [fromRouteId] + const visitedRouteIds = new Set() + while (pendingRouteIds.length > 0) { + const routeId = pendingRouteIds.pop()! + if (routeId === targetRouteId) return true + if (visitedRouteIds.has(routeId)) continue + visitedRouteIds.add(routeId) + pendingRouteIds.push( + ...(this.failedOwnerPairCounts.get(routeId)?.keys() ?? []), + ) + } + return false + } + private publishSelectiveReripStats(): void { const failedOwnerPairs: FailedOwnerPairCount[] = [] for (const [failedRouteId, ownerCounts] of this.failedOwnerPairCounts) { 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 8d1b2b5..7e683bc 100644 --- a/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts +++ b/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts @@ -8,6 +8,12 @@ import { import type { PortId, RegionId, RouteId } from "lib/types" class TestOutsideInPartialRipSolver extends OutsideInPartialRipTinyHyperGraphSolver { + preferredPreservedRouteIds = new Set() + + protected override getRouteIdsPreferredForPreservation() { + return this.preferredPreservedRouteIds + } + prepare(hotRegionIds: RegionId[], regionCosts: Float64Array): boolean { return this.preparePartialRip(hotRegionIds, regionCosts) } @@ -86,6 +92,49 @@ test("partial rip preserves both outside route ends", () => { expect(solver.stats.retainedPartialRipSegmentCount).toBe(3) }) +test("partial rip leaves preferred routes unchanged when another hot route can move", () => { + const solver = createLinearSolver(24, {}, 2) + solver.preferredPreservedRouteIds.add(0) + for (let regionId = 1; regionId <= 4; regionId++) { + const [routeZeroSegment] = solver.state.regionSegments[regionId]! + solver.state.regionSegments[regionId]!.push([ + 1, + routeZeroSegment![1], + routeZeroSegment![2], + ]) + } + const regionCosts = new Float64Array(6) + regionCosts[3] = 1 + + expect(solver.prepare([3], regionCosts)).toBe(true) + expect(solver.state.unroutedRoutes).toEqual([1]) + expect( + solver.state.regionSegments.flat().filter(([routeId]) => routeId === 0), + ).toEqual([ + [0, 0, 1], + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + ]) +}) + +test("partial rip locally rerips a preferred route when every hot route is preferred", () => { + const solver = createLinearSolver() + solver.preferredPreservedRouteIds.add(0) + const regionCosts = new Float64Array(6) + regionCosts[3] = 1 + + expect(solver.prepare([3], regionCosts)).toBe(true) + expect(solver.getActiveEndpoints(0)).toEqual([2, 3]) + expect(solver.state.unroutedRoutes).toEqual([0]) + expect(solver.state.regionSegments.flat()).toEqual([ + [0, 0, 1], + [0, 1, 2], + [0, 3, 4], + ]) + expect(solver.stats.partialRipCount).toBe(1) +}) + test("a near-target initial solution selects the larger quality window", () => { const nearTargetSolver = createLinearSolver() nearTargetSolver.PARTIAL_RIP_QUALITY_MAX_DISTANCE = 8 diff --git a/tests/selective-rerip-tiny-hyper-graph-solver.test.ts b/tests/selective-rerip-tiny-hyper-graph-solver.test.ts index 86cf46d..d9b6062 100644 --- a/tests/selective-rerip-tiny-hyper-graph-solver.test.ts +++ b/tests/selective-rerip-tiny-hyper-graph-solver.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { orderRoutesAfterSelectiveRerip, + SelectiveReripTinyHyperGraphSolver, selectOwnerRouteIdsToRip, } from "lib/selective-rerip-tiny-hyper-graph-solver" @@ -32,3 +33,80 @@ test("keeps pending routes ahead of newly ripped routes", () => { }), ).toEqual([7, 3, 8, 9, 4, 2]) }) + +test("prefers another blocker owner over a route marked for preservation", () => { + const searches: number[][] = [] + class SolverWithPreferredPreservedRoute extends SelectiveReripTinyHyperGraphSolver { + protected override getRouteIdsPreferredForPreservation() { + return new Set([1]) + } + + protected override findRelaxedBlockerPath( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ) { + searches.push([...forbiddenOwnerRouteIds]) + return { + found: true as const, + states: [], + hops: [], + owners: new Set(forbiddenOwnerRouteIds.has(1) ? [2] : [1]), + distance: 1, + expandedLabelCount: 1, + } + } + + findPreferredBlockerPath() { + return this.findRelaxedBlockerPathPreferringPreservedRoutes() + } + } + const solver = Object.create( + SolverWithPreferredPreservedRoute.prototype, + ) as SolverWithPreferredPreservedRoute + + const result = solver.findPreferredBlockerPath() + + expect(searches).toEqual([[1]]) + expect(result.found && [...result.owners]).toEqual([2]) +}) + +test("rerips a preserved route when no other blocker path exists", () => { + const searches: number[][] = [] + class SolverWithUnavoidablePreservedRoute extends SelectiveReripTinyHyperGraphSolver { + protected override getRouteIdsPreferredForPreservation() { + return new Set([1]) + } + + protected override findRelaxedBlockerPath( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ) { + searches.push([...forbiddenOwnerRouteIds]) + if (forbiddenOwnerRouteIds.has(1)) { + return { + found: false as const, + reason: "no_path" as const, + expandedLabelCount: 1, + } + } + return { + found: true as const, + states: [], + hops: [], + owners: new Set([1]), + distance: 1, + expandedLabelCount: 1, + } + } + + findPreferredBlockerPath() { + return this.findRelaxedBlockerPathPreferringPreservedRoutes() + } + } + const solver = Object.create( + SolverWithUnavoidablePreservedRoute.prototype, + ) as SolverWithUnavoidablePreservedRoute + + const result = solver.findPreferredBlockerPath() + + expect(searches).toEqual([[1], []]) + expect(result.found && [...result.owners]).toEqual([1]) +}) diff --git a/tests/solver/bugreport87-unused-port-repro.test.ts b/tests/solver/bugreport87-unused-port-repro.test.ts index b4e0141..ba41316 100644 --- a/tests/solver/bugreport87-unused-port-repro.test.ts +++ b/tests/solver/bugreport87-unused-port-repro.test.ts @@ -18,7 +18,7 @@ const repro = fixture as unknown as { const REPRO_TIMEOUT_MS = 30_000 -test.failing( +test( "repro: unused port triggers a repeated selective rerip cycle", () => { const { topology, problem } = loadSerializedHyperGraph( diff --git a/tests/solver/trace-density-region-cost.test.ts b/tests/solver/trace-density-region-cost.test.ts new file mode 100644 index 0000000..b910a36 --- /dev/null +++ b/tests/solver/trace-density-region-cost.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { computeRegionCost } from "lib/computeRegionCost" + +test("trace-density cost penalizes concentrated parallel routes", () => { + const sparseCost = computeRegionCost(2, 2, 0, 0, 0, 2, 0, 0.3, 1) + const denseCost = computeRegionCost(2, 2, 0, 0, 0, 8, 0, 0.3, 1) + const singleLayerDenseCost = computeRegionCost(2, 2, 0, 0, 0, 8, 1, 0.3, 1) + const twoLayerDenseCost = computeRegionCost(2, 2, 0, 0, 0, 8, 3, 0.3, 1) + const legacyDenseCost = computeRegionCost(2, 2, 0, 0, 0, 8) + + expect(legacyDenseCost).toBe(0) + expect(sparseCost).toBeGreaterThan(0) + expect(denseCost).toBeGreaterThan(sparseCost) + expect(singleLayerDenseCost).toBeGreaterThan(twoLayerDenseCost) +})