diff --git a/lib/core.ts b/lib/core.ts index 4fc2b9c..09c861a 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -220,6 +220,8 @@ interface SegmentGeometryScratch { export class TinyHyperGraphSolver extends BaseSolver { state: TinyHyperGraphWorkingState private _problemSetup?: TinyHyperGraphProblemSetup + private edgeDeltaCostByKey = new Map() + protected enableEdgeDeltaCostCache = false private segmentGeometryScratch: SegmentGeometryScratch = { lesserAngle: 0, greaterAngle: 0, @@ -318,6 +320,12 @@ export class TinyHyperGraphSolver extends BaseSolver { void this.problemSetup } + protected clearEdgeDeltaCostCache() { + if (this.enableEdgeDeltaCostCache) { + this.edgeDeltaCostByKey.clear() + } + } + override _step() { const { problem, topology, state } = this @@ -331,6 +339,7 @@ export class TinyHyperGraphSolver extends BaseSolver { state.currentRouteNetId = problem.routeNet[state.currentRouteId!] this.resetCandidateBestCosts() + this.clearEdgeDeltaCostCache() const startingPortId = problem.routeStartPort[state.currentRouteId!] state.candidateQueue.clear() const startingNextRegionId = this.getStartingNextRegionId( @@ -377,8 +386,7 @@ export class TinyHyperGraphSolver extends BaseSolver { return } - const neighbors = - topology.regionIncidentPorts[currentCandidate.nextRegionId] + const neighbors = topology.regionIncidentPorts[currentCandidate.nextRegionId] for (const neighborPortId of neighbors) { const assignedRouteId = state.portAssignment[neighborPortId] @@ -399,7 +407,6 @@ export class TinyHyperGraphSolver extends BaseSolver { const g = this.computeG(currentCandidate, neighborPortId) if (!Number.isFinite(g)) continue - const h = this.computeH(neighborPortId) const nextRegionId = topology.incidentPortRegion[neighborPortId][0] === @@ -414,7 +421,12 @@ export class TinyHyperGraphSolver extends BaseSolver { continue } - const newCandidate = { + const candidateHopId = this.getHopId(neighborPortId, nextRegionId) + if (g >= this.getCandidateBestCost(candidateHopId)) continue + + const h = this.computeH(neighborPortId) + this.setCandidateBestCost(candidateHopId, g) + state.candidateQueue.queue({ prevRegionId: currentCandidate.nextRegionId, nextRegionId, portId: neighborPortId, @@ -422,18 +434,7 @@ export class TinyHyperGraphSolver extends BaseSolver { h, f: g + h, prevCandidate: currentCandidate, - } - - if (neighborPortId === state.goalPortId) { - this.onPathFound(newCandidate) - return - } - - const candidateHopId = this.getHopId(neighborPortId, nextRegionId) - if (g >= this.getCandidateBestCost(candidateHopId)) continue - - this.setCandidateBestCost(candidateHopId, g) - state.candidateQueue.queue(newCandidate) + }) } } @@ -660,6 +661,7 @@ export class TinyHyperGraphSolver extends BaseSolver { resetRoutingStateForRerip() { const { topology, problem, state } = this + this.clearEdgeDeltaCostCache() state.portAssignment.fill(-1) state.regionSegments = Array.from( { length: topology.regionCount }, @@ -770,60 +772,115 @@ export class TinyHyperGraphSolver extends BaseSolver { this.appendSegmentToRegionCache(regionId, fromPortId, toPortId) } + this.clearEdgeDeltaCostCache() state.candidateQueue.clear() state.currentRouteNetId = undefined state.currentRouteId = undefined } computeG(currentCandidate: Candidate, neighborPortId: PortId): number { - const { topology, state } = this - - const nextRegionId = currentCandidate.nextRegionId - - const regionCache = state.regionIntersectionCaches[nextRegionId] + const segmentRegionId = currentCandidate.nextRegionId const segmentGeometry = this.populateSegmentGeometryScratch( - nextRegionId, + segmentRegionId, currentCandidate.portId, neighborPortId, ) + const edgeDeltaCost = this.computeEdgeDeltaCost( + segmentRegionId, + this.getEdgeCostKey( + segmentRegionId, + currentCandidate.portId, + neighborPortId, + ), + segmentGeometry.lesserAngle, + segmentGeometry.greaterAngle, + segmentGeometry.layerMask, + segmentGeometry.entryExitLayerChanges, + ) + if (!Number.isFinite(edgeDeltaCost)) { + return Number.POSITIVE_INFINITY + } + + return ( + currentCandidate.g + + edgeDeltaCost + + this.state.regionCongestionCost[segmentRegionId] + ) + } + + protected getEdgeCostKey( + segmentRegionId: RegionId, + port1Id: PortId, + port2Id: PortId, + ) { + const lesserPortId = port1Id < port2Id ? port1Id : port2Id + const greaterPortId = port1Id < port2Id ? port2Id : port1Id + + return ( + segmentRegionId * this.topology.portCount * this.topology.portCount + + lesserPortId * this.topology.portCount + + greaterPortId + ) + } + + protected computeEdgeDeltaCost( + segmentRegionId: RegionId, + edgeCostKey: number, + lesserAngle: number, + greaterAngle: number, + layerMask: number, + entryExitLayerChanges: number, + ) { + const cachedEdgeDeltaCost = this.enableEdgeDeltaCostCache + ? this.edgeDeltaCostByKey.get(edgeCostKey) + : undefined + + if (cachedEdgeDeltaCost !== undefined) { + return cachedEdgeDeltaCost + } + + const regionCache = this.state.regionIntersectionCaches[segmentRegionId] const [ newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, ] = countNewIntersectionsWithValues( regionCache, - state.currentRouteNetId!, - segmentGeometry.lesserAngle, - segmentGeometry.greaterAngle, - segmentGeometry.layerMask, - segmentGeometry.entryExitLayerChanges, + this.state.currentRouteNetId!, + lesserAngle, + greaterAngle, + layerMask, + entryExitLayerChanges, ) if ( newSameLayerIntersections > 0 && - this.isKnownSingleLayerRegion(nextRegionId) + this.isKnownSingleLayerRegion(segmentRegionId) ) { + if (this.enableEdgeDeltaCostCache) { + this.edgeDeltaCostByKey.set(edgeCostKey, Number.POSITIVE_INFINITY) + } return Number.POSITIVE_INFINITY } const newRegionCost = computeRegionCost( - topology.regionWidth[nextRegionId], - topology.regionHeight[nextRegionId], + this.topology.regionWidth[segmentRegionId], + this.topology.regionHeight[segmentRegionId], regionCache.existingSameLayerIntersections + newSameLayerIntersections, regionCache.existingCrossingLayerIntersections + newCrossLayerIntersections, regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges, regionCache.existingSegmentCount + 1, - topology.regionAvailableZMask?.[nextRegionId] ?? 0, + this.topology.regionAvailableZMask?.[segmentRegionId] ?? 0, ) - regionCache.existingRegionCost - return ( - currentCandidate.g + - newRegionCost + - state.regionCongestionCost[nextRegionId] - ) + if (this.enableEdgeDeltaCostCache) { + this.edgeDeltaCostByKey.set(edgeCostKey, newRegionCost) + } + + return newRegionCost } computeH(neighborPortId: PortId): number { diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index fdc3ce5..0efef51 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -11,7 +11,11 @@ import type { import { TinyHyperGraphSolver } from "../core" import type { RegionId } from "../types" import type { TinyHyperGraphSectionSolverOptions } from "./index" -import { getActiveSectionRouteIds, TinyHyperGraphSectionSolver } from "./index" +import { + getActiveSectionRouteIds, + getOrderedRoutePaths, + TinyHyperGraphSectionSolver, +} from "./index" /** * Candidate section families used by the automatic section-mask search. @@ -89,6 +93,21 @@ const getMaxRegionCost = (solver: TinyHyperGraphSolver) => 0, ) +const summarizeSolverRegionCosts = ( + solver: TinyHyperGraphSolver, +): { maxRegionCost: number; totalRegionCost: number } => + solver.state.regionIntersectionCaches.reduce( + (summary, regionIntersectionCache) => ({ + maxRegionCost: Math.max( + summary.maxRegionCost, + regionIntersectionCache.existingRegionCost, + ), + totalRegionCost: + summary.totalRegionCost + regionIntersectionCache.existingRegionCost, + }), + { maxRegionCost: 0, totalRegionCost: 0 }, + ) + const getSerializedOutputMaxRegionCost = ( serializedHyperGraph: SerializedHyperGraph, ) => { @@ -235,15 +254,13 @@ const findBestAutomaticSectionMask = ( ): AutomaticSectionSearchResult => { const searchStartTime = performance.now() const baselineEvaluationStartTime = performance.now() - const baselineSectionSolver = new TinyHyperGraphSectionSolver( - topology, - problem, - solution, - sectionSolverOptions, - ) - const baselineMaxRegionCost = getMaxRegionCost( - baselineSectionSolver.baselineSolver, - ) + const orderedRoutePaths = getOrderedRoutePaths(topology, problem, solution) + const sharedBaseline = { + baselineSolver: solvedSolver, + baselineSummary: summarizeSolverRegionCosts(solvedSolver), + orderedRoutePaths, + } + const baselineMaxRegionCost = sharedBaseline.baselineSummary.maxRegionCost const baselineEvaluationMs = performance.now() - baselineEvaluationStartTime let bestFinalMaxRegionCost = baselineMaxRegionCost @@ -293,6 +310,7 @@ const findBestAutomaticSectionMask = ( topology, candidateProblem, solution, + orderedRoutePaths, ) candidateEligibilityMs += performance.now() - eligibilityStartTime @@ -308,6 +326,7 @@ const findBestAutomaticSectionMask = ( candidateProblem, solution, sectionSolverOptions, + sharedBaseline, ) candidateInitMs += performance.now() - candidateInitStartTime @@ -425,6 +444,13 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver("solveGraph") @@ -448,6 +474,12 @@ export class TinyHyperGraphSectionPipelineSolver extends BasePipelineSolver { +): OrderedRoutePath => { const routeSegments = solution.solvedRoutePathSegments[routeId] ?? [] const routeSegmentRegionIds = solution.solvedRoutePathRegionIds?.[routeId] ?? [] const startPortId = problem.routeStartPort[routeId] @@ -342,6 +351,15 @@ const getOrderedRoutePath = ( } } +export const getOrderedRoutePaths = ( + topology: TinyHyperGraphTopology, + problem: TinyHyperGraphProblem, + solution: TinyHyperGraphSolution, +): OrderedRoutePath[] => + Array.from({ length: problem.routeCount }, (_, routeId) => + getOrderedRoutePath(topology, problem, solution, routeId), + ) + const applyRouteSegmentsToSolver = ( solver: TinyHyperGraphSolver, routeSegmentsByRegion: Array<[RouteId, PortId, PortId][]>, @@ -436,6 +454,7 @@ const createSectionRoutePlans = ( topology: TinyHyperGraphTopology, problem: TinyHyperGraphProblem, solution: TinyHyperGraphSolution, + orderedRoutePaths?: OrderedRoutePath[], ): { sectionProblem: TinyHyperGraphProblem routePlans: SectionRoutePlan[] @@ -454,12 +473,9 @@ const createSectionRoutePlans = ( for (let routeId = 0; routeId < problem.routeCount; routeId++) { const routePlan = routePlans[routeId]! - const { orderedPortIds, orderedRegionIds } = getOrderedRoutePath( - topology, - problem, - solution, - routeId, - ) + const { orderedPortIds, orderedRegionIds } = + orderedRoutePaths?.[routeId] ?? + getOrderedRoutePath(topology, problem, solution, routeId) const maskedRuns: Array<{ startIndex: number; endIndex: number }> = [] let currentRunStartIndex: number | undefined @@ -560,7 +576,10 @@ export const getActiveSectionRouteIds = ( topology: TinyHyperGraphTopology, problem: TinyHyperGraphProblem, solution: TinyHyperGraphSolution, -) => createSectionRoutePlans(topology, problem, solution).activeRouteIds + orderedRoutePaths?: OrderedRoutePath[], +) => + createSectionRoutePlans(topology, problem, solution, orderedRoutePaths) + .activeRouteIds const getSectionRegionIds = ( topology: TinyHyperGraphTopology, @@ -592,6 +611,8 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { MAX_RIPS = Number.POSITIVE_INFINITY MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT = Number.POSITIVE_INFINITY EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST = Number.POSITIVE_INFINITY + EDGE_DELTA_COST_CACHE_MAX_ACTIVE_ROUTES = 12 + EDGE_DELTA_COST_CACHE_MAX_MUTABLE_REGIONS = 24 constructor( topology: TinyHyperGraphTopology, @@ -605,6 +626,9 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { ) { super(topology, problem, options) applyTinyHyperGraphSectionSolverOptions(this, options) + this.enableEdgeDeltaCostCache = + activeRouteIds.length <= this.EDGE_DELTA_COST_CACHE_MAX_ACTIVE_ROUTES && + mutableRegionIds.length <= this.EDGE_DELTA_COST_CACHE_MAX_MUTABLE_REGIONS this.state.unroutedRoutes = [...activeRouteIds] this.applyFixedSegments() this.fixedSnapshot = cloneSolvedStateSnapshot({ @@ -675,6 +699,43 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { return super.getStartingNextRegionId(routeId, startingPortId) } + override computeProblemSetup(): TinyHyperGraphProblemSetup { + const { topology, problem } = this + const portX = topology.portX as unknown as ArrayLike + const portY = topology.portY as unknown as ArrayLike + const portHCostToEndOfRoute = new Float64Array( + topology.portCount * problem.routeCount, + ) + const portEndpointNetIds = Array.from( + { length: topology.portCount }, + () => new Set(), + ) + + for (const routeId of this.activeRouteIds) { + const routeNetId = problem.routeNet[routeId] + const startPortId = problem.routeStartPort[routeId] + const endPortId = problem.routeEndPort[routeId] + + portEndpointNetIds[startPortId]!.add(routeNetId) + portEndpointNetIds[endPortId]!.add(routeNetId) + + const endX = portX[endPortId] + const endY = portY[endPortId] + + for (let portId = 0; portId < topology.portCount; portId++) { + const dx = portX[portId] - endX + const dy = portY[portId] - endY + portHCostToEndOfRoute[portId * problem.routeCount + routeId] = + Math.hypot(dx, dy) * this.DISTANCE_TO_COST + } + } + + return { + portHCostToEndOfRoute, + portEndpointNetIds, + } + } + override resetRoutingStateForRerip() { if (!this.fixedSnapshot) { super.resetRoutingStateForRerip() @@ -686,6 +747,7 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { return } + this.clearEdgeDeltaCostCache() restoreSolvedStateSnapshot(this, this.fixedSnapshot) this.state.currentRouteId = undefined this.state.currentRouteNetId = undefined @@ -856,6 +918,7 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { optimizedSolver?: TinyHyperGraphSolver sectionSolver?: TinyHyperGraphSectionSearchSolver activeRouteIds: RouteId[] = [] + orderedRoutePaths: OrderedRoutePath[] DISTANCE_TO_COST = 0.05 @@ -875,18 +938,26 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { public problem: TinyHyperGraphProblem, public initialSolution: TinyHyperGraphSolution, options?: TinyHyperGraphSectionSolverOptions, + sharedContext?: TinyHyperGraphSectionSharedContext, ) { super() applyTinyHyperGraphSectionSolverOptions(this, options) - this.baselineSolver = createSolvedSolverFromSolution( - topology, - problem, - initialSolution, - getTinyHyperGraphSolverOptions(this), - ) - this.baselineSummary = summarizeRegionIntersectionCaches( - this.baselineSolver.state.regionIntersectionCaches, - ) + this.baselineSolver = + sharedContext?.baselineSolver ?? + createSolvedSolverFromSolution( + topology, + problem, + initialSolution, + getTinyHyperGraphSolverOptions(this), + ) + this.baselineSummary = + sharedContext?.baselineSummary ?? + summarizeRegionIntersectionCaches( + this.baselineSolver.state.regionIntersectionCaches, + ) + this.orderedRoutePaths = + sharedContext?.orderedRoutePaths ?? + getOrderedRoutePaths(topology, problem, initialSolution) this.sectionRegionIds = getSectionRegionIds(topology, problem) this.sectionBaselineSummary = summarizeRegionIntersectionCachesForRegionIds( this.baselineSolver.state.regionIntersectionCaches, @@ -913,7 +984,12 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { this.applySectionRipPolicy() const { sectionProblem, routePlans, activeRouteIds } = - createSectionRoutePlans(this.topology, this.problem, this.initialSolution) + createSectionRoutePlans( + this.topology, + this.problem, + this.initialSolution, + this.orderedRoutePaths, + ) this.activeRouteIds = activeRouteIds diff --git a/package.json b/package.json index 308b956..0c795c5 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "start": "cosmos", "typecheck": "tsc -p tsconfig.json --pretty false", "benchmark1": "bun run --cpu-prof-md scripts/profiling/hg07-first10.ts", + "profile:section": "bun run scripts/profiling/section-solver-hotspots.ts", "benchmark:section": "bun run scripts/benchmarking/hg07-section-pipeline.ts", "benchmark:section:profile": "bun run scripts/benchmarking/hg07-first40-section-profile.ts", "benchmark:port-point-pathing": "bun run scripts/benchmarking/port-point-pathing-section-pipeline.ts" diff --git a/scripts/profiling/section-solver-hotspots.ts b/scripts/profiling/section-solver-hotspots.ts new file mode 100644 index 0000000..e44b253 --- /dev/null +++ b/scripts/profiling/section-solver-hotspots.ts @@ -0,0 +1,508 @@ +import { existsSync, readFileSync } from "node:fs" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import * as datasetHg07 from "dataset-hg07" +import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" +import { + convertPortPointPathingSolverInputToSerializedHyperGraph, + TinyHyperGraphSectionPipelineSolver, + TinyHyperGraphSectionSolver, + TinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphSectionCandidateFamily, + type TinyHyperGraphSectionSolverOptions, + type TinyHyperGraphSolution, + type TinyHyperGraphTopology, +} from "../../lib/index" + +type DatasetModule = Record & { + manifest: { + sampleCount: number + samples: Array<{ + sampleName: string + circuitKey: string + circuitId: string + stepsToPortPointSolve: number + }> + } +} + +type ProfileMode = "pipeline-search" | "fixed-candidate" + +const DEFAULT_INPUT_PATH = + process.env.TINY_HYPERGRAPH_PORT_POINT_PATHING_INPUT ?? + "/Users/seve/Downloads/portPointPathingSolver_input (6).json" + +const DEFAULT_CANDIDATE_FAMILIES: TinyHyperGraphSectionCandidateFamily[] = [ + "self-touch", + "onehop-all", + "onehop-touch", + "twohop-all", + "twohop-touch", +] + +const DEFAULT_SECTION_SOLVER_OPTIONS: TinyHyperGraphSectionSolverOptions = { + DISTANCE_TO_COST: 0.05, + RIP_THRESHOLD_RAMP_ATTEMPTS: 16, + RIP_CONGESTION_REGION_COST_FACTOR: 0.1, + MAX_ITERATIONS: 1e6, + MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT: 6, + EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST: Number.POSITIVE_INFINITY, +} + +const datasetModule = datasetHg07 as DatasetModule + +const parseStringArg = (flag: string) => { + const argIndex = process.argv.findIndex((arg) => arg === flag) + return argIndex === -1 ? undefined : process.argv[argIndex + 1] +} + +const parsePositiveIntegerArg = (flag: string, fallback: number) => { + const rawValue = parseStringArg(flag) + if (!rawValue) { + return fallback + } + + const parsedValue = Number(rawValue) + if (!Number.isInteger(parsedValue) || parsedValue <= 0) { + throw new Error(`Invalid ${flag} value: ${rawValue}`) + } + + return parsedValue +} + +const parseCandidateFamiliesArg = () => { + const rawFamilies = parseStringArg("--families") + if (!rawFamilies) { + return DEFAULT_CANDIDATE_FAMILIES + } + + return rawFamilies + .split(",") + .map((family) => family.trim()) + .filter(Boolean) as TinyHyperGraphSectionCandidateFamily[] +} + +const round = (value: number, digits = 3) => Number(value.toFixed(digits)) + +const getAdjacentRegionIds = ( + topology: TinyHyperGraphTopology, + seedRegionIds: number[], +) => { + const adjacentRegionIds = new Set(seedRegionIds) + + for (const seedRegionId of seedRegionIds) { + for (const portId of topology.regionIncidentPorts[seedRegionId] ?? []) { + for (const regionId of topology.incidentPortRegion[portId] ?? []) { + adjacentRegionIds.add(regionId) + } + } + } + + return [...adjacentRegionIds] +} + +const createPortSectionMaskForRegionIds = ( + topology: TinyHyperGraphTopology, + regionIds: number[], + portSelectionRule: + | "touches-selected-region" + | "all-incident-regions-selected", +) => { + const selectedRegionIds = new Set(regionIds) + + return Int8Array.from({ length: topology.portCount }, (_, portId) => { + const incidentRegionIds = topology.incidentPortRegion[portId] ?? [] + + if (portSelectionRule === "touches-selected-region") { + return incidentRegionIds.some((regionId) => selectedRegionIds.has(regionId)) + ? 1 + : 0 + } + + return incidentRegionIds.length > 0 && + incidentRegionIds.every((regionId) => selectedRegionIds.has(regionId)) + ? 1 + : 0 + }) +} + +const createProblemWithPortSectionMask = ( + problem: TinyHyperGraphProblem, + portSectionMask: Int8Array, +): TinyHyperGraphProblem => ({ + routeCount: problem.routeCount, + portSectionMask, + routeMetadata: problem.routeMetadata, + routeStartPort: new Int32Array(problem.routeStartPort), + routeEndPort: new Int32Array(problem.routeEndPort), + routeNet: new Int32Array(problem.routeNet), + regionNetId: new Int32Array(problem.regionNetId), +}) + +const getCandidatePortSectionMask = ( + solvedSolver: TinyHyperGraphSolver, + topology: TinyHyperGraphTopology, + family: TinyHyperGraphSectionCandidateFamily, + maxHotRegions: number, + hotIndex = 0, +) => { + const hotRegionIds = solvedSolver.state.regionIntersectionCaches + .map((regionIntersectionCache, regionId) => ({ + regionId, + regionCost: regionIntersectionCache.existingRegionCost, + })) + .filter(({ regionCost }) => regionCost > 0) + .sort((left, right) => right.regionCost - left.regionCost) + .slice(0, maxHotRegions) + .map(({ regionId }) => regionId) + + const hotRegionId = hotRegionIds[hotIndex] + if (hotRegionId === undefined) { + throw new Error(`No hot region at index ${hotIndex}`) + } + + const oneHopRegionIds = getAdjacentRegionIds(topology, [hotRegionId]) + const twoHopRegionIds = getAdjacentRegionIds(topology, oneHopRegionIds) + + const candidateByFamily: Record< + TinyHyperGraphSectionCandidateFamily, + { + label: string + portSectionMask: Int8Array + } + > = { + "self-touch": { + label: `hot-${hotRegionId}-self-touch`, + portSectionMask: createPortSectionMaskForRegionIds( + topology, + [hotRegionId], + "touches-selected-region", + ), + }, + "onehop-all": { + label: `hot-${hotRegionId}-onehop-all`, + portSectionMask: createPortSectionMaskForRegionIds( + topology, + oneHopRegionIds, + "all-incident-regions-selected", + ), + }, + "onehop-touch": { + label: `hot-${hotRegionId}-onehop-touch`, + portSectionMask: createPortSectionMaskForRegionIds( + topology, + oneHopRegionIds, + "touches-selected-region", + ), + }, + "twohop-all": { + label: `hot-${hotRegionId}-twohop-all`, + portSectionMask: createPortSectionMaskForRegionIds( + topology, + twoHopRegionIds, + "all-incident-regions-selected", + ), + }, + "twohop-touch": { + label: `hot-${hotRegionId}-twohop-touch`, + portSectionMask: createPortSectionMaskForRegionIds( + topology, + twoHopRegionIds, + "touches-selected-region", + ), + }, + } + + return candidateByFamily[family] +} + +const getSerializedInput = (): SerializedHyperGraph => { + const source = parseStringArg("--source") ?? "attached" + + if (source === "attached") { + const inputPath = parseStringArg("--input") ?? DEFAULT_INPUT_PATH + if (!existsSync(inputPath)) { + throw new Error( + `Input file not found at ${inputPath}. Pass --input or set TINY_HYPERGRAPH_PORT_POINT_PATHING_INPUT.`, + ) + } + + const input = JSON.parse(readFileSync(inputPath, "utf8")) + return convertPortPointPathingSolverInputToSerializedHyperGraph(input) + } + + if (source === "hg07") { + const sampleName = parseStringArg("--sample") ?? "sample032" + const serializedHyperGraph = datasetModule[sampleName] as + | SerializedHyperGraph + | undefined + if (!serializedHyperGraph) { + throw new Error(`Unknown hg07 sample: ${sampleName}`) + } + return serializedHyperGraph + } + + throw new Error(`Unknown --source value: ${source}`) +} + +const getMaxRegionCost = (solver: TinyHyperGraphSolver) => + solver.state.regionIntersectionCaches.reduce( + (maxRegionCost, regionIntersectionCache) => + Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), + 0, + ) + +const getSolvedReplayContext = ( + serializedHyperGraph: SerializedHyperGraph, +): { + solvedSolver: TinyHyperGraphSolver + topology: TinyHyperGraphTopology + problem: TinyHyperGraphProblem + solution: TinyHyperGraphSolution +} => { + const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) + const solvedSolver = new TinyHyperGraphSolver(topology, problem) + solvedSolver.solve() + + if (solvedSolver.failed || !solvedSolver.solved) { + throw new Error(solvedSolver.error ?? "solveGraph failed unexpectedly") + } + + const replay = loadSerializedHyperGraph(solvedSolver.getOutput()) + return { + solvedSolver, + topology: replay.topology, + problem: replay.problem, + solution: replay.solution, + } +} + +const runPipelineSearchProfile = ( + serializedHyperGraph: SerializedHyperGraph, + repeatCount: number, + maxHotRegions: number, + candidateFamilies: TinyHyperGraphSectionCandidateFamily[], +) => { + const rows = [] + + for (let runIndex = 0; runIndex < repeatCount; runIndex++) { + const pipelineStartTime = performance.now() + const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ + serializedHyperGraph, + sectionSearchConfig: { + maxHotRegions, + candidateFamilies, + }, + }) + pipelineSolver.solve() + + if (pipelineSolver.failed) { + throw new Error( + pipelineSolver.error ?? "section pipeline search failed unexpectedly", + ) + } + + rows.push({ + run: runIndex + 1, + pipelineMs: round(performance.now() - pipelineStartTime), + sectionSearchMs: round(Number(pipelineSolver.stats.sectionSearchMs ?? 0)), + candidateCount: Number( + pipelineSolver.stats.sectionSearchCandidateCount ?? 0, + ), + family: pipelineSolver.selectedSectionCandidateFamily ?? null, + label: pipelineSolver.selectedSectionCandidateLabel ?? null, + finalMaxRegionCost: round( + Number(pipelineSolver.stats.sectionSearchFinalMaxRegionCost ?? 0), + 12, + ), + }) + } + + console.log("section solver hotspot profile") + console.log( + JSON.stringify( + { + mode: "pipeline-search", + repeatCount, + maxHotRegions, + candidateFamilies, + }, + null, + 2, + ), + ) + console.table(rows) +} + +const runFixedCandidateProfile = ( + serializedHyperGraph: SerializedHyperGraph, + repeatCount: number, + maxHotRegions: number, + candidateFamilies: TinyHyperGraphSectionCandidateFamily[], +) => { + const context = getSolvedReplayContext(serializedHyperGraph) + const selectionPipeline = new TinyHyperGraphSectionPipelineSolver({ + serializedHyperGraph, + sectionSearchConfig: { + maxHotRegions, + candidateFamilies, + }, + }) + selectionPipeline.solve() + + if (selectionPipeline.failed || !selectionPipeline.selectedSectionMask) { + throw new Error( + selectionPipeline.error ?? + "section pipeline did not select a fixed candidate", + ) + } + + const rows = [] + + for (let runIndex = 0; runIndex < repeatCount; runIndex++) { + const sectionSolver = new TinyHyperGraphSectionSolver( + context.topology, + createProblemWithPortSectionMask( + context.problem, + selectionPipeline.selectedSectionMask, + ), + context.solution, + DEFAULT_SECTION_SOLVER_OPTIONS, + ) + + const startTime = performance.now() + sectionSolver.solve() + const elapsedMs = performance.now() - startTime + + if (sectionSolver.failed || !sectionSolver.solved) { + throw new Error( + sectionSolver.error ?? "fixed section candidate failed unexpectedly", + ) + } + + rows.push({ + run: runIndex + 1, + solveMs: round(elapsedMs), + activeRouteCount: sectionSolver.activeRouteIds.length, + ripCount: Number(sectionSolver.stats.ripCount ?? 0), + optimized: Boolean(sectionSolver.stats.optimized), + finalMaxRegionCost: round( + Number(sectionSolver.stats.finalMaxRegionCost ?? 0), + 12, + ), + solvedMaxRegionCost: round(getMaxRegionCost(sectionSolver.getSolvedSolver()), 12), + }) + } + + console.log("section solver hotspot profile") + console.log( + JSON.stringify( + { + mode: "fixed-candidate", + repeatCount, + maxHotRegions, + candidateFamilies, + selectedSectionCandidateFamily: + selectionPipeline.selectedSectionCandidateFamily ?? null, + selectedSectionCandidateLabel: + selectionPipeline.selectedSectionCandidateLabel ?? null, + }, + null, + 2, + ), + ) + console.table(rows) +} + +const runExplicitCandidateProfile = ( + serializedHyperGraph: SerializedHyperGraph, + repeatCount: number, + maxHotRegions: number, +) => { + const family = parseStringArg( + "--candidate-family", + ) as TinyHyperGraphSectionCandidateFamily | null + if (!family) { + throw new Error( + "Explicit candidate mode requires --candidate-family ", + ) + } + + const hotIndex = parsePositiveIntegerArg("--hot-index", 1) - 1 + const context = getSolvedReplayContext(serializedHyperGraph) + const candidate = getCandidatePortSectionMask( + context.solvedSolver, + context.topology, + family, + maxHotRegions, + hotIndex, + ) + const rows = [] + + for (let runIndex = 0; runIndex < repeatCount; runIndex++) { + const sectionSolver = new TinyHyperGraphSectionSolver( + context.topology, + createProblemWithPortSectionMask(context.problem, candidate.portSectionMask), + context.solution, + DEFAULT_SECTION_SOLVER_OPTIONS, + ) + + const startTime = performance.now() + sectionSolver.solve() + const elapsedMs = performance.now() - startTime + + rows.push({ + run: runIndex + 1, + solveMs: round(elapsedMs), + activeRouteCount: sectionSolver.activeRouteIds.length, + ripCount: Number(sectionSolver.stats.ripCount ?? 0), + optimized: Boolean(sectionSolver.stats.optimized), + finalMaxRegionCost: round( + Number(sectionSolver.stats.finalMaxRegionCost ?? 0), + 12, + ), + }) + } + + console.log("section solver hotspot profile") + console.log( + JSON.stringify( + { + mode: "fixed-candidate", + repeatCount, + maxHotRegions, + candidateFamily: family, + candidateLabel: candidate.label, + }, + null, + 2, + ), + ) + console.table(rows) +} + +const mode = (parseStringArg("--mode") ?? "pipeline-search") as ProfileMode +const repeatCount = parsePositiveIntegerArg("--repeat", 3) +const maxHotRegions = parsePositiveIntegerArg("--max-hot-regions", 2) +const candidateFamilies = parseCandidateFamiliesArg() +const serializedHyperGraph = getSerializedInput() + +if (parseStringArg("--candidate-family")) { + runExplicitCandidateProfile(serializedHyperGraph, repeatCount, maxHotRegions) +} else if (mode === "pipeline-search") { + runPipelineSearchProfile( + serializedHyperGraph, + repeatCount, + maxHotRegions, + candidateFamilies, + ) +} else if (mode === "fixed-candidate") { + runFixedCandidateProfile( + serializedHyperGraph, + repeatCount, + maxHotRegions, + candidateFamilies, + ) +} else { + throw new Error(`Unknown --mode value: ${mode}`) +} diff --git a/tests/profiling/section-solver-hotspots.test.ts b/tests/profiling/section-solver-hotspots.test.ts new file mode 100644 index 0000000..8d811ed --- /dev/null +++ b/tests/profiling/section-solver-hotspots.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test" + +test("section solver hotspot profiling script runs successfully", () => { + const result = Bun.spawnSync( + [ + "bun", + "run", + "scripts/profiling/section-solver-hotspots.ts", + "--source", + "hg07", + "--sample", + "sample032", + "--mode", + "fixed-candidate", + "--repeat", + "1", + "--max-hot-regions", + "2", + ], + { + cwd: process.cwd(), + stdout: "pipe", + stderr: "pipe", + }, + ) + + const stdout = new TextDecoder().decode(result.stdout) + const stderr = new TextDecoder().decode(result.stderr) + + expect(result.exitCode).toBe(0) + expect(stderr).toBe("") + expect(stdout).toContain("section solver hotspot profile") + expect(stdout).toContain("\"mode\": \"fixed-candidate\"") +})