From 028b065932eb5c9e5d6884dbea169d0ef6d42236 Mon Sep 17 00:00:00 2001 From: antra-tess Date: Mon, 21 Sep 2026 12:08:44 -0700 Subject: [PATCH 1/2] perf(kv-unified): pack label storage and selectively rescore terminal cuts Preserve exact floors, selection, ties, and hysteresis while reusing bucket storage and avoiding unnecessary terminal leaf scans. Keep the object backend and exhaustive scoring as diagnostic references. Co-Authored-By: Codex (GPT-6) --- .../kv-unified-packed-selective.changed.md | 6 + docs/kv-unified-packed-selective.md | 99 +++++ scripts/benchmark-kv-unified-general.mjs | 24 +- scripts/replay-kv-unified-messages.mjs | 28 +- src/adaptive/kv-unified-packed-storage.ts | 197 +++++++++ src/adaptive/kv-unified-packed.ts | 398 ++++++++++++++++++ src/adaptive/kv-unified-pareto.ts | 18 +- src/adaptive/kv-unified-policy.ts | 31 +- src/adaptive/kv-unified-selective.ts | 135 ++++++ src/adaptive/kv-unified-terminal.ts | 142 ++++++- test/adaptive/kv-unified-packed.test.ts | 142 +++++++ test/adaptive/kv-unified-selective.test.ts | 85 ++++ 12 files changed, 1277 insertions(+), 28 deletions(-) create mode 100644 changelog.d/kv-unified-packed-selective.changed.md create mode 100644 docs/kv-unified-packed-selective.md create mode 100644 src/adaptive/kv-unified-packed-storage.ts create mode 100644 src/adaptive/kv-unified-packed.ts create mode 100644 src/adaptive/kv-unified-selective.ts create mode 100644 test/adaptive/kv-unified-packed.test.ts create mode 100644 test/adaptive/kv-unified-selective.test.ts diff --git a/changelog.d/kv-unified-packed-selective.changed.md b/changelog.d/kv-unified-packed-selective.changed.md new file mode 100644 index 00000000..c48ed569 --- /dev/null +++ b/changelog.d/kv-unified-packed-selective.changed.md @@ -0,0 +1,6 @@ +- Use reusable packed label records and bucket tables in the kv-unified DAG + solver. Preserve the object implementation as a diagnostic reference. +- Select terminal candidates with conservative rounding bounds and exact cache + accounting, refining all possible winners, ties, and normalization-floor + witnesses. Keep the selected result exact and defer remaining diagnostic + candidate-list evaluations until requested, without changing policy or grids. diff --git a/docs/kv-unified-packed-selective.md b/docs/kv-unified-packed-selective.md new file mode 100644 index 00000000..3e39d638 --- /dev/null +++ b/docs/kv-unified-packed-selective.md @@ -0,0 +1,99 @@ +# Packed propagation and selective exact scoring + +This follow-up to PR #110 targets the two remaining measured costs: label +storage/grouping and repeated terminal leaf scans. It does not change policy +weights, grids, hard budgets, adoption thresholds, or label ceilings, and it +does not introduce a solve deadline. The certificate remains disabled in the +performance checks. + +## Storage + +The default DAG engine uses integer label handles and fixed-size binary64 +records. Branches copy one contiguous record; transforms own their handles; +pruning returns rejected slots to a free list. Cache keys and comparison +fields are grouped in the first half of each record. Traces are immutable +parent/action indices in paged uint32 storage; pending assignments are only +materialized when needed by a surviving path or a branch. + +Bucket lookup reuses generation-stamped numeric tables. Group membership and +representative handles are stored in reusable integer arrays, so each prune +does not allocate a Map entry and member array for every bucket. Unbounded or +non-numeric key spaces use sparse lookup with the same packed members; the +dense allocation limit only chooses storage and never changes the candidate +set or stops a solve. + +Packed batches own their handles. Branching clones before either path can +mutate the original. Pruning reads every source envelope before updating a +retained representative, then releases only discarded handles. The object +engine remains available as `storage: 'objects'` for differential checks. + +## Exact selection without every leaf scan + +Each trace action caches its per-leaf fidelity/continuity sum, identity match, +and first emitted index for each render unit. A candidate estimate combines +those action sums instead of rescanning every leaf. + +The cache cost is **exact**, not an estimate from propagation state: emitted +units are deduplicated, their earliest assigned leaf indices are sorted, and +the current token offsets and extension costs are accumulated in the same +chronological order as the exact evaluator. This handles gap-bearing ownership, +protected holes, changed receipt token costs, and fractional token counts. + +Fidelity and continuity use conservative binary64 intervals. Both the grouped +sum and the exact evaluator sum the same nonnegative, precomputed floating-point +leaf terms. For a complete disjoint cut of n leaves, grouping uses at most 2n +additions and the exact evaluation at most n. The bound uses +`16 * Number.EPSILON * (n + 1)`, an outward-rounded error and endpoints, and a +subnormal allowance. If the bound is not usable, exact evaluation is used. +This is a rounding bound on a specific retained cut, not the solver's much +larger bucket-approximation error bound. + +Selection proceeds in three stages: + +1. Compute the exact cache floor. Refine every candidate whose continuity + interval could lower the best exact continuity floor, even if that candidate + is otherwise a welfare loser. +2. With both floors fixed, evaluate score intervals using the same monotone + nonnegative score expression as the original scorer. Find upper bounds for + the best candidate and the best candidate matching the accepted presentation. +3. Exactly evaluate every candidate whose lower bound could meet either upper + bound. Apply the original score/token/frontier tie-break and hysteresis rule. + +The selected candidate, both floors, and its score are exact. Public candidate +list inspection performs any remaining exact evaluations and returns the +complete sorted list, including the same selected object. Normal solving does +not inspect that diagnostic list. Captured budgets and token costs are retained +if callers later mutate the inputs. + +`terminalEvaluation: 'full'` forces exhaustive rescoring on the packed engine. +`exactTerminalEvaluations` reports evaluations needed during the solve, before +later diagnostic-list inspection. + +## Validation and benchmarking + +Differential tests compare object storage, packed/full, and packed/selective +on varied forests, including every retained candidate, both normalization +floors, selected scores/frontiers, and all existing approximation-envelope +statistics. They also check intervals against every feasible small-forest cut, +cache corrections against full rendering, floor-setting welfare losers, exact +ties and hysteresis boundaries, non-finite fallback behavior, snapshots, and +label-slot reuse and growth. + +Use the saved structural fixture without opening a resident store: + +```sh +bun scripts/benchmark-kv-unified-general.mjs --fixture /path/to/solver-inputs.json \ + --objects --warm --budget 500000 --report /path/to/reference.json +bun scripts/benchmark-kv-unified-general.mjs --fixture /path/to/solver-inputs.json \ + --full-scoring --warm --budget 500000 --report /path/to/packed-full.json +bun scripts/benchmark-kv-unified-general.mjs --fixture /path/to/solver-inputs.json \ + --warm --budget 500000 --report /path/to/packed-selective.json +``` + +`--verify-estimates` exactly evaluates every estimate and asserts that its +fidelity/continuity intervals contain the exact result and that cache churn +and presentation identity match. Its timing is diagnostic, not the normal +selective-scoring timing. + +The implementation is on a separate follow-up branch; no live runtime or +resident configuration was changed. diff --git a/scripts/benchmark-kv-unified-general.mjs b/scripts/benchmark-kv-unified-general.mjs index d16a2fdf..527edbdb 100644 --- a/scripts/benchmark-kv-unified-general.mjs +++ b/scripts/benchmark-kv-unified-general.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs'; import { createHash } from 'node:crypto'; import { CanonicalSummaryForest } from '../src/adaptive/kv-unified.ts'; import { ParetoKvUnifiedPolicySolver } from '../src/adaptive/kv-unified-pareto.ts'; +import { TerminalPolicyEvaluator } from '../src/adaptive/kv-unified-terminal.ts'; const arg = (name) => { const index = process.argv.indexOf(name); @@ -12,6 +13,21 @@ if (!arg('--fixture')) throw new Error('--fixture PATH required'); const fixture = JSON.parse(fs.readFileSync(arg('--fixture'), 'utf8'), (_key, value) => value?.$map ? new Map(value.$map) : value?.$set ? new Set(value.$set) : value); const { inputs } = fixture; +let verifiedEstimates = 0; +if (process.argv.includes('--verify-estimates')) { + const estimate = TerminalPolicyEvaluator.prototype.estimate; + TerminalPolicyEvaluator.prototype.estimate = function (...args) { + const bounded = estimate.apply(this, args); + const exact = bounded.exact(); + if (!(bounded.fidelity.lower <= exact.fidelityLoss && exact.fidelityLoss <= bounded.fidelity.upper) || + !(bounded.continuity.lower <= exact.continuityLoss && exact.continuityLoss <= bounded.continuity.upper) || + bounded.cacheChurn !== exact.cacheChurn || bounded.matchesPresentation !== exact.matchesPresentation) { + throw new Error('terminal estimate did not enclose the exact candidate'); + } + verifiedEstimates++; + return bounded; + }; +} const changes = []; if (process.argv.includes('--pin')) { const chunk = inputs.chunks.find((chunk, index) => index > inputs.chunks.length / 2 && @@ -37,6 +53,8 @@ const phases = []; const diagnosticStop = new Error('diagnostic stopped after propagation; no layout selected'); const options = { ...fixture.options, hysteresisCertificate: false, + ...(process.argv.includes('--objects') ? { storage: 'objects' } : {}), + ...(process.argv.includes('--full-scoring') ? { terminalEvaluation: 'full' } : {}), ...(arg('--budget') ? { maxTokens: Number(arg('--budget')) } : {}), ...(process.argv.includes('--warm') ? { currentImmutablePrefixHash: fixture.options.cache?.immutablePrefixHash } : {}), ...(process.argv.includes('--initial') ? { presentation: undefined, cache: undefined } : {}), @@ -51,12 +69,14 @@ const started = performance.now(); const forest = new CanonicalSummaryForest(inputs, fixture.forestOptions); try { const result = new ParetoKvUnifiedPolicySolver(inputs, forest).solve(options); + const selectedFrontier = result.feasible ? result.selected.frontier : undefined; const report = { ms: performance.now() - started, rssMB: process.memoryUsage().rss / 1e6, feasible: result.feasible, phases, maxTokens: options.maxTokens, warm: Boolean(options.cache && options.currentImmutablePrefixHash === options.cache.immutablePrefixHash), - initial: !options.presentation, changes, certificateEnabled: false }; + initial: !options.presentation, changes, certificateEnabled: false, + verifiedEstimates, diagnosticVerification: process.argv.includes('--verify-estimates') }; if (result.feasible) { - const frontier = [...result.selected.frontier].sort(([a], [b]) => a.localeCompare(b)); + const frontier = [...selectedFrontier].sort(([a], [b]) => a.localeCompare(b)); Object.assign(report, { tokens: result.selected.renderedTokens, score: result.selected.score, cacheFloor: result.cacheFloor, continuityFloor: result.continuityFloor, diff --git a/scripts/replay-kv-unified-messages.mjs b/scripts/replay-kv-unified-messages.mjs index 1c5a1816..ffaa6850 100644 --- a/scripts/replay-kv-unified-messages.mjs +++ b/scripts/replay-kv-unified-messages.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import { CanonicalSummaryForest } from '../src/adaptive/kv-unified.ts'; import { ParetoKvUnifiedPolicySolver } from '../src/adaptive/kv-unified-pareto.ts'; import { SummaryTree } from '../src/adaptive/summary-tree.ts'; @@ -19,6 +20,8 @@ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); const full = [...fixture.inputs.chunks].sort((a, b) => a.sequence - b.sequence); const count = Number(arg('--count') ?? 500); const limit = Number(arg('--limit') ?? count); +const verifyEvery = Number(arg('--verify-every') ?? 0); +if (!Number.isInteger(verifyEvery) || verifyEvery < 0) throw new Error('invalid verify-every'); if (!Number.isInteger(count) || count < 1 || count >= full.length || !Number.isInteger(limit) || limit < 1 || limit > count) { throw new Error('invalid count/limit'); } @@ -128,6 +131,7 @@ const config = { toTimestamp: new Date(metadata.messages[first + limit - 1].timestamp).toISOString(), prefixMessages: first, snapshotMessages: full.length, maxTokens: fixture.options.maxTokens, tailWindowTokens: metadata.recentWindowTokens, certificateEnabled: false, + referenceVerification: verifyEvery > 0 ? `object engine every ${verifyEvery} samples, at warm-up, and on every movement; outside solve timing` : 'disabled', presentation: 'one initial warm-up excluded; each successful message-prefix solve accepted before the next', cache: 'previous accepted atomic layout; fresh token-weighted markers; stable immutable prefix; no wall-clock TTL simulation', summaries: 'snapshot catalogue, exclude future-source summaries; not historical compression-time reconstruction', @@ -160,6 +164,7 @@ function summary() { medianMs: quantile(0.5), p90Ms: quantile(0.9), p95Ms: quantile(0.95), p99Ms: quantile(0.99), maxMs: times.at(-1) ?? null, histogram, maxRssMB: Math.max(0, ...rows.map((row) => row.rssMB)), movementSolves: rows.filter((row) => row.ok && row.moves > 0).length, + referenceChecks: rows.filter((row) => row.referenceVerified).length, slowest: rows.filter((row) => row.ok).sort((a, b) => b.solveMs - a.solveMs).slice(0, 10), updatedAt: new Date().toISOString(), }; @@ -173,7 +178,7 @@ for (let step = 0; step <= limit; step++) { const solveStarted = performance.now(); try { const forest = new CanonicalSummaryForest(inputs, fixture.forestOptions); - const result = new ParetoKvUnifiedPolicySolver(inputs, forest).solve({ + const solveOptions = { ...fixture.options, hysteresisCertificate: false, presentation, cache, currentImmutablePrefixHash: cache ? prefixHash : undefined, onProgress: (event) => { @@ -182,7 +187,8 @@ for (let step = 0; step <= limit; step++) { lastProgress = performance.now(); } }, - }); + }; + const result = new ParetoKvUnifiedPolicySolver(inputs, forest).solve(solveOptions); if (!result.feasible) throw new Error(`infeasible: ${JSON.stringify(result.feasibility)}`); const frontier = result.selected.frontier; row.solveMs = performance.now() - solveStarted; @@ -191,6 +197,24 @@ for (let step = 0; step <= limit; step++) { row.moves = inputs.chunks.filter((chunk) => (frontier.get(chunk.id) ?? 0) !== chunk.currentResolution).length; row.labels = result.propagation?.terminalLabels; row.maxLabelsPerState = result.propagation?.maxLabelsPerState; + row.exactTerminalEvaluations = result.propagation?.exactTerminalEvaluations; + const decision = (solved) => ({ + tokens: solved.selected.renderedTokens, score: solved.selected.score, + fidelityLoss: solved.selected.fidelityLoss, continuityLoss: solved.selected.continuityLoss, + cacheChurn: solved.selected.cacheChurn, cacheFloor: solved.cacheFloor, continuityFloor: solved.continuityFloor, + frontierHash: createHash('sha256').update(JSON.stringify([...solved.selected.frontier] + .sort(([a], [b]) => a.localeCompare(b)))).digest('hex'), + }); + Object.assign(row, decision(result)); + if (verifyEvery > 0 && (step % verifyEvery === 0 || row.moves > 0)) { + const referenceStarted = performance.now(); + const reference = new ParetoKvUnifiedPolicySolver(inputs, forest).solve({ ...solveOptions, storage: 'objects', onProgress: undefined }); + if (!reference.feasible || JSON.stringify(decision(reference)) !== JSON.stringify(decision(result))) { + throw new Error(`object-engine decision mismatch at step ${step}`); + } + row.referenceVerified = true; + row.referenceMs = performance.now() - referenceStarted; + } sequence++; const leaves = new Map(); for (const chunk of inputs.chunks) { diff --git a/src/adaptive/kv-unified-packed-storage.ts b/src/adaptive/kv-unified-packed-storage.ts new file mode 100644 index 00000000..08218d82 --- /dev/null +++ b/src/adaptive/kv-unified-packed-storage.ts @@ -0,0 +1,197 @@ +import type { FrontierTraceSource } from './kv-unified-terminal.js'; + +export interface PackedEmission { + readonly kind: 'raw' | 'recall'; + readonly key: string; + readonly tokens: number; + readonly sequence: number; +} + +const TRACE_SHIFT = 18; +const TRACE_SIZE = 1 << TRACE_SHIFT; +const TRACE_MASK = TRACE_SIZE - 1; +const EMPTY_EMISSIONS: readonly PackedEmission[] = Object.freeze([]); + +export const LABEL_STRIDE = 16; +export enum LabelField { + Tokens, Extension, Continuity, Fidelity, + Intact, CachedUnits, Matched, CachedTokens, + TokenError, ContinuityError, FidelityError, CacheError, + Trace, PendingAction, +} + +/** Immutable trace nodes use two integers. Pages avoid copying a growing + * arena; rejected labels do not allocate a trace node until it is needed. */ +export class PackedTraceArena { + private parents: Uint32Array[] = []; + private actions: Uint32Array[] = []; + private definitions: Array<{ ids: readonly string[]; level: number }> = [{ ids: [], level: 0 }]; + private length = 0; + + action(ids: readonly string[], level: number): number { + this.definitions.push({ ids, level }); + return this.definitions.length - 1; + } + + append(parent: number, action: number): number { + if (action === 0) return parent; + const id = ++this.length; + if (id > 0xffffffff) throw new Error('kv-unified trace index exhausted'); + const page = id >>> TRACE_SHIFT; + while (this.parents.length <= page) { + this.parents.push(new Uint32Array(TRACE_SIZE)); + this.actions.push(new Uint32Array(TRACE_SIZE)); + } + this.parents[page][id & TRACE_MASK] = parent; + this.actions[page][id & TRACE_MASK] = action; + return id; + } + + reference(id: number): FrontierTraceSource { + const arena = this; + return { + forEachAssignment(visit) { + for (let cursor = id; cursor !== 0;) { + const page = cursor >>> TRACE_SHIFT; + const offset = cursor & TRACE_MASK; + const action = arena.definitions[arena.actions[page][offset]]; + visit(action.ids, action.level); + cursor = arena.parents[page][offset]; + } + }, + }; + } + + get nodes(): number { return this.length; } +} + +/** Owned labels are integer handles into reusable packed numeric records. A batch + * owns its handles: transforms may update them, pruning frees discarded ones, + * and branching explicitly clones. Trace nodes remain immutable. */ +export class PackedLabels { + private capacity = 1024; + private length = 0; + private free = 0; + private freeNext = new Uint32Array(this.capacity); + readonly traces = new PackedTraceArena(); + + // One contiguous record permits native copyWithin for branching. Integer + // handles are uint32-sized and exactly representable in binary64. + data = new Float64Array(this.capacity * LABEL_STRIDE); + pending: Array = []; + + private grow(): void { + const capacity = this.capacity * 2; + if (capacity > 0x7fffffff) throw new Error('kv-unified label index exhausted'); + const data = new Float64Array(capacity * LABEL_STRIDE); data.set(this.data); this.data = data; + const free = new Uint32Array(capacity); free.set(this.freeNext); this.freeNext = free; + this.capacity = capacity; + } + + allocate(): number { + if (this.free) { + const id = this.free; this.free = this.freeNext[id]; return id; + } + const id = ++this.length; + if (id >= this.capacity) this.grow(); + return id; + } + + initial(cacheRelevant: boolean, externalIds: readonly string[]): number { + const id = this.allocate(); + const at = id * LABEL_STRIDE; + this.data.fill(0, at, at + LABEL_STRIDE); + this.data[at + LabelField.Intact] = cacheRelevant ? 1 : 0; + this.pending[id] = EMPTY_EMISSIONS; + if (externalIds.length) this.data[at + LabelField.PendingAction] = this.traces.action(externalIds, 0); + return id; + } + + clone(source: number): number { + this.finishTrace(source); + const id = this.allocate(); + this.data.copyWithin(id * LABEL_STRIDE, source * LABEL_STRIDE, (source + 1) * LABEL_STRIDE); + this.pending[id] = this.pending[source]; + return id; + } + + release(id: number): void { + this.pending[id] = EMPTY_EMISSIONS; + this.freeNext[id] = this.free; this.free = id; + } + + finishTrace(id: number): number { + const at = id * LABEL_STRIDE; + if (this.data[at + LabelField.PendingAction]) { + this.data[at + LabelField.Trace] = this.traces.append(this.data[at + LabelField.Trace], this.data[at + LabelField.PendingAction]); + this.data[at + LabelField.PendingAction] = 0; + } + return this.data[at + LabelField.Trace]; + } + + assign(id: number, action: number, fidelity: number, continuity: number): void { + if (!action) return; + this.finishTrace(id); + const at = id * LABEL_STRIDE; + this.data[at + LabelField.PendingAction] = action; + this.data[at + LabelField.Fidelity] += fidelity; + this.data[at + LabelField.Continuity] += continuity; + } + + get slots(): number { return this.length; } +} + +/** Reusable grouping metadata. Large/non-numeric key spaces use a Map only + * for key-to-group lookup; member chains and extrema remain packed. */ +export class PackedBuckets { + private epoch = 0; + private readonly epochs: Uint32Array; + private readonly slots: Uint32Array; + private readonly sparse = new Map(); + count = 0; + head = new Int32Array(1024); + next = new Int32Array(1024); + fidelity = new Uint32Array(1024); + continuity = new Uint32Array(1024); + tokens = new Uint32Array(1024); + extension = new Uint32Array(1024); + size = new Uint32Array(1024); + + constructor(denseSlots: number) { + // This only selects the representation, never the number of retained + // labels or the solver's stopping condition. + const dense = Number.isSafeInteger(denseSlots) && denseSlots > 0 && denseSlots <= 4_000_000 ? denseSlots : 0; + this.epochs = new Uint32Array(dense); + this.slots = new Uint32Array(dense); + } + + begin(labels: number): void { + this.count = 0; + this.sparse.clear(); + this.epoch = (this.epoch + 1) >>> 0; + if (this.epoch === 0) { this.epochs.fill(0); this.epoch = 1; } + if (labels > this.next.length) this.next = new Int32Array(Math.max(labels, this.next.length * 2)); + } + + group(key: string | number): number { + const dense = typeof key === 'number' && Number.isInteger(key) && key >= 0 && key < this.epochs.length; + if (dense && this.epochs[key] === this.epoch) return this.slots[key]; + if (!dense) { + const found = this.sparse.get(key); + if (found !== undefined) return found; + } + const group = this.count++; + if (group >= this.head.length) { + const length = this.head.length * 2; + const head = new Int32Array(length); head.set(this.head); this.head = head; + for (const key of ['fidelity', 'continuity', 'tokens', 'extension', 'size'] as const) { + const copy = new Uint32Array(length); copy.set(this[key]); this[key] = copy; + } + } + this.head[group] = -1; this.size[group] = 0; + this.fidelity[group] = this.continuity[group] = this.tokens[group] = this.extension[group] = 0; + if (dense) { this.epochs[key] = this.epoch; this.slots[key] = group; } + else this.sparse.set(key, group); + return group; + } +} diff --git a/src/adaptive/kv-unified-packed.ts b/src/adaptive/kv-unified-packed.ts new file mode 100644 index 00000000..792582d1 --- /dev/null +++ b/src/adaptive/kv-unified-packed.ts @@ -0,0 +1,398 @@ +import type { PickerInputs } from './picker.js'; +import { CanonicalSummaryForest, SparseLabelCeilingError, type MinimumTokenResult } from './kv-unified.js'; +import { + ExactKvUnifiedPolicySolver, continuityLeafLoss, fidelityLeafLoss, normalizePolicy, +} from './kv-unified-policy.js'; +import { TerminalPolicyEvaluator, type FrontierTraceReference } from './kv-unified-terminal.js'; +import { scoreBoundedCandidates } from './kv-unified-selective.js'; +import { PackedBuckets, PackedLabels, LABEL_STRIDE, LabelField } from './kv-unified-packed-storage.js'; +import type { ApproximationEnvelope, ParetoPolicySolveResult, ParetoSolveOptions } from './kv-unified-pareto.js'; + +interface Action { + trace: number; + ids: readonly string[]; + level: number; + fidelity: number; + continuity: number; + tokens: number; + extension: number; + key?: string; + sequence: number; +} + +/** The same select/expand/grid algorithm as the object reference engine, + * using owned numeric handles and reusable bucket metadata. */ +export class PackedDagSolver { + private readonly s = new PackedLabels(); + private readonly leaves; + private readonly chunks; + private readonly age = new Map(); + private readonly newest: number; + private readonly policy; + private readonly cacheRelevant: boolean; + private readonly markers: Map; + private readonly cacheClasses: Map; + private readonly buckets: PackedBuckets; + private readonly tBucket: number; + private readonly kBucket: number; + private readonly fBucket: number; + private kBins = 0; + private fBins = 0; + private gridKeys = 0; + private readonly numericCache: boolean; + private readonly started = performance.now(); + private states = 0; + private created = 1; + private expanded = 0; + private dominated = 0; + private maximum = 1; + private errorScratch = new Float64Array(16); + private readonly members: number[] = []; + private readonly representatives: number[] = []; + + constructor(private readonly inputs: PickerInputs, private readonly forest: CanonicalSummaryForest, + private readonly options: ParetoSolveOptions, private readonly buffered: boolean, + private readonly approximationBound: (error: ApproximationEnvelope) => number, + private readonly knownFeasibility: Extract) { + this.policy = normalizePolicy(options.policy); + this.leaves = forest.orderedLeaves(); + this.chunks = new Map(inputs.chunks.map((chunk) => [chunk.id, chunk])); + this.newest = inputs.chunks.reduce((value, chunk) => Math.max(value, chunk.sequence), 0); + let age = 0; + for (let i = this.leaves.length - 1; i >= 0; i--) { + const leaf = this.leaves[i]; this.age.set(leaf.id, age + leaf.rawTokens / 2); age += leaf.rawTokens; + } + this.cacheRelevant = options.cache !== undefined && options.currentImmutablePrefixHash !== undefined && + options.cache.immutablePrefixHash === options.currentImmutablePrefixHash; + this.markers = new Map((options.cache?.markers ?? []).map((marker) => [marker.unitIndex, marker.offset])); + this.cacheClasses = new Map([...this.markers.keys()].filter((index) => index > 0).sort((a, b) => a - b) + .map((index, i) => [index, i + 1])); + this.tBucket = Math.max(0, Math.floor(options.tokenBucketSize ?? 0)); + this.kBucket = Math.max(0, options.continuityBucketSize ?? 0); + this.fBucket = Math.max(0, options.fidelityBucketSize ?? 0); + if (this.tBucket > 0 && this.kBucket > 0 && this.fBucket > 0) { + let maxK = 0, maxF = 0; + for (const leaf of this.leaves) { + const level = Math.max(...leaf.availableLevels); + const previous = options.presentation?.leaves.get(leaf.id)?.level ?? 0; + maxK += leaf.rawTokens * Math.max(1, previous, Math.abs(level - previous)); + if (!leaf.externallyAccounted) maxF += fidelityLeafLoss(this.chunks.get(leaf.id)!, level, this.newest, this.policy); + } + this.kBins = Math.ceil(maxK / this.kBucket) + 2; + this.fBins = Math.ceil(maxF / this.fBucket) + 2; + const keys = (Math.ceil(options.maxTokens / this.tBucket) + 2) * this.kBins * this.fBins; + if (Number.isSafeInteger(keys)) this.gridKeys = keys; + else this.kBins = this.fBins = 0; + } + const cacheKeys = this.gridKeys * (this.cacheClasses.size + 1); + this.numericCache = this.gridKeys > 0 && Number.isSafeInteger(cacheKeys); + this.buckets = new PackedBuckets(this.cacheRelevant ? (this.numericCache ? cacheKeys : 0) : this.gridKeys); + } + + private key(id: number): string | number { + const s = this.s; + const t = this.tBucket > 0 ? Math.ceil(s.data[(id) * LABEL_STRIDE + LabelField.Tokens] / this.tBucket) : s.data[(id) * LABEL_STRIDE + LabelField.Tokens]; + const k = this.kBucket > 0 ? Math.floor(s.data[(id) * LABEL_STRIDE + LabelField.Continuity] / this.kBucket) : 'c*'; + const f = this.fBucket > 0 ? Math.floor(s.data[(id) * LABEL_STRIDE + LabelField.Fidelity] / this.fBucket) : 'f*'; + if (this.cacheRelevant && s.data[(id) * LABEL_STRIDE + LabelField.Intact]) return [ + '0', t, 1, s.data[(id) * LABEL_STRIDE + LabelField.Matched], s.data[(id) * LABEL_STRIDE + LabelField.CachedTokens], + s.pending[id].map((emission) => `${emission.sequence}/${emission.kind}/${emission.key}`).join(','), k, f, + ].join(':'); + const numeric = this.kBins > 0 && typeof k === 'number' && typeof f === 'number' && + k >= 0 && k < this.kBins && f >= 0 && f < this.fBins; + const grid = numeric ? (t * this.kBins + k) * this.fBins + f : `${t}:${k}:${f}`; + if (!this.cacheRelevant) return grid; + const c = this.cacheClasses.get(s.data[(id) * LABEL_STRIDE + LabelField.CachedUnits]) ?? 0; + return this.numericCache && typeof grid === 'number' ? c * this.gridKeys + grid : `broken:${c}:${grid}`; + } + + private order(a: number, b: number): number { + const s = this.s; + return s.data[(a) * LABEL_STRIDE + LabelField.Fidelity] - s.data[(b) * LABEL_STRIDE + LabelField.Fidelity] || s.data[(a) * LABEL_STRIDE + LabelField.Continuity] - s.data[(b) * LABEL_STRIDE + LabelField.Continuity] || s.data[(a) * LABEL_STRIDE + LabelField.Tokens] - s.data[(b) * LABEL_STRIDE + LabelField.Tokens] || + (this.cacheRelevant ? s.data[(b) * LABEL_STRIDE + LabelField.Extension] - s.data[(a) * LABEL_STRIDE + LabelField.Extension] : 0) || s.data[(a) * LABEL_STRIDE + LabelField.Matched] - s.data[(b) * LABEL_STRIDE + LabelField.Matched]; + } + + private dominates(a: number, b: number): boolean { + const s = this.s; + return (!this.cacheRelevant || s.data[(a) * LABEL_STRIDE + LabelField.Extension] >= s.data[(b) * LABEL_STRIDE + LabelField.Extension]) && s.data[(a) * LABEL_STRIDE + LabelField.Tokens] <= s.data[(b) * LABEL_STRIDE + LabelField.Tokens] && + s.data[(a) * LABEL_STRIDE + LabelField.Continuity] <= s.data[(b) * LABEL_STRIDE + LabelField.Continuity] && s.data[(a) * LABEL_STRIDE + LabelField.Fidelity] <= s.data[(b) * LABEL_STRIDE + LabelField.Fidelity] && + (s.data[(a) * LABEL_STRIDE + LabelField.Tokens] < s.data[(b) * LABEL_STRIDE + LabelField.Tokens] || s.data[(a) * LABEL_STRIDE + LabelField.Continuity] < s.data[(b) * LABEL_STRIDE + LabelField.Continuity] || s.data[(a) * LABEL_STRIDE + LabelField.Fidelity] < s.data[(b) * LABEL_STRIDE + LabelField.Fidelity] || + (this.cacheRelevant && s.data[(a) * LABEL_STRIDE + LabelField.Extension] > s.data[(b) * LABEL_STRIDE + LabelField.Extension])); + } + + private prune(incoming: number[]): number[] { + const s = this.s, b = this.buckets; + this.states++; + b.begin(incoming.length); + const grid = this.kBucket > 0 && this.fBucket > 0; + for (let pos = 0; pos < incoming.length; pos++) { + const id = incoming[pos]; + if (s.data[(id) * LABEL_STRIDE + LabelField.Tokens] > this.options.maxTokens) { s.release(id); continue; } + const group = b.group(this.key(id)); + b.next[pos] = b.head[group]; b.head[group] = pos; + if (b.size[group]++ === 0) b.fidelity[group] = b.continuity[group] = b.tokens[group] = b.extension[group] = id; + else if (grid) { + if (this.order(id, b.fidelity[group]) < 0) b.fidelity[group] = id; + if ((s.data[(id) * LABEL_STRIDE + LabelField.Continuity] - s.data[(b.continuity[group]) * LABEL_STRIDE + LabelField.Continuity] || this.order(id, b.continuity[group])) < 0) b.continuity[group] = id; + if ((s.data[(id) * LABEL_STRIDE + LabelField.Tokens] - s.data[(b.tokens[group]) * LABEL_STRIDE + LabelField.Tokens] || this.order(id, b.tokens[group])) < 0) b.tokens[group] = id; + if (this.cacheRelevant && (s.data[(b.extension[group]) * LABEL_STRIDE + LabelField.Extension] - s.data[(id) * LABEL_STRIDE + LabelField.Extension] || this.order(id, b.extension[group])) < 0) b.extension[group] = id; + } + } + const result: number[] = []; + for (let group = 0; group < b.count; group++) { + if (b.size[group] === 1) { result.push(incoming[b.head[group]]); continue; } + const kept = this.representatives; kept.length = 0; + if (grid) { + const f = b.fidelity[group], k = b.continuity[group], t = b.tokens[group], e = b.extension[group]; + kept.push(f); + if (k !== f) kept.push(k); + if (t !== f && t !== k) kept.push(t); + if (this.cacheRelevant && e !== f && e !== k && e !== t) kept.push(e); + } else { + const pool = this.members; pool.length = 0; + for (let pos = b.head[group]; pos >= 0; pos = b.next[pos]) pool.push(incoming[pos]); + pool.reverse(); + for (const id of pool) if (!pool.some((other) => other !== id && this.dominates(other, id))) kept.push(id); + } + if (this.errorScratch.length < kept.length * 4) this.errorScratch = new Float64Array(kept.length * 8); + const errors = this.errorScratch; + for (let i = 0; i < kept.length; i++) { + const id = kept[i]; + errors[i * 4] = s.data[(id) * LABEL_STRIDE + LabelField.TokenError]; errors[i * 4 + 1] = s.data[(id) * LABEL_STRIDE + LabelField.ContinuityError]; + errors[i * 4 + 2] = s.data[(id) * LABEL_STRIDE + LabelField.FidelityError]; errors[i * 4 + 3] = s.data[(id) * LABEL_STRIDE + LabelField.CacheError]; + } + // Read every original envelope before writing any representative. The + // max reductions are order-independent; ties still use F/K/T/E order. + for (let pos = b.head[group]; pos >= 0; pos = b.next[pos]) { + const source = incoming[pos]; + let chosen = 0, best = Infinity; + for (let i = 0; i < kept.length; i++) { + const target = kept[i]; + const cost = Math.abs(s.data[(target) * LABEL_STRIDE + LabelField.Tokens] - s.data[(source) * LABEL_STRIDE + LabelField.Tokens]) + + Math.max(0, s.data[(target) * LABEL_STRIDE + LabelField.Continuity] - s.data[(source) * LABEL_STRIDE + LabelField.Continuity]) + + Math.max(0, s.data[(target) * LABEL_STRIDE + LabelField.Fidelity] - s.data[(source) * LABEL_STRIDE + LabelField.Fidelity]) + + (this.cacheRelevant ? Math.max(0, s.data[(source) * LABEL_STRIDE + LabelField.Extension] - s.data[(target) * LABEL_STRIDE + LabelField.Extension]) : 0); + if (cost < best) { best = cost; chosen = i; } + } + const target = kept[chosen], at = chosen * 4; + errors[at] = Math.max(errors[at], s.data[(source) * LABEL_STRIDE + LabelField.TokenError] + Math.abs(s.data[(target) * LABEL_STRIDE + LabelField.Tokens] - s.data[(source) * LABEL_STRIDE + LabelField.Tokens])); + errors[at + 1] = Math.max(errors[at + 1], s.data[(source) * LABEL_STRIDE + LabelField.ContinuityError] + Math.max(0, s.data[(target) * LABEL_STRIDE + LabelField.Continuity] - s.data[(source) * LABEL_STRIDE + LabelField.Continuity])); + errors[at + 2] = Math.max(errors[at + 2], s.data[(source) * LABEL_STRIDE + LabelField.FidelityError] + Math.max(0, s.data[(target) * LABEL_STRIDE + LabelField.Fidelity] - s.data[(source) * LABEL_STRIDE + LabelField.Fidelity])); + if (this.cacheRelevant) errors[at + 3] = Math.max(errors[at + 3], s.data[(source) * LABEL_STRIDE + LabelField.CacheError] + Math.max(0, s.data[(source) * LABEL_STRIDE + LabelField.Extension] - s.data[(target) * LABEL_STRIDE + LabelField.Extension])); + } + for (let i = 0; i < kept.length; i++) { + const id = kept[i]; + s.data[(id) * LABEL_STRIDE + LabelField.TokenError] = errors[i * 4]; s.data[(id) * LABEL_STRIDE + LabelField.ContinuityError] = errors[i * 4 + 1]; + s.data[(id) * LABEL_STRIDE + LabelField.FidelityError] = errors[i * 4 + 2]; s.data[(id) * LABEL_STRIDE + LabelField.CacheError] = errors[i * 4 + 3]; + result.push(id); + } + for (let pos = b.head[group]; pos >= 0; pos = b.next[pos]) if (!kept.includes(incoming[pos])) s.release(incoming[pos]); + this.dominated += b.size[group] - kept.length; + } + this.created += result.length; + this.maximum = Math.max(this.maximum, result.length); + const ceiling = this.options.labelCeiling ?? 1_000_000; + if (result.length > ceiling) throw new SparseLabelCeilingError(ceiling, result.length); + if (this.states % 1000 === 0) this.progress('propagate', result.length); + return result; + } + + private progress(phase: string, labels: number): void { + this.options.onProgress?.({ phase, labels, states: this.states, elapsedMs: performance.now() - this.started }); + } + + private emit(id: number, kind: 'head' | 'raw' | 'recall' | 'tail', key: string, + tokens: number, extension: boolean, sequence = 0): void { + const s = this.s; + s.data[(id) * LABEL_STRIDE + LabelField.Tokens] += tokens; + if (extension) s.data[(id) * LABEL_STRIDE + LabelField.Extension] += tokens; + if (this.buffered && s.data[(id) * LABEL_STRIDE + LabelField.Intact] && (kind === 'raw' || kind === 'recall')) { + s.pending[id] = [...s.pending[id], { kind, key, tokens, sequence }]; return; + } + if (s.data[(id) * LABEL_STRIDE + LabelField.Intact]) { + const previous = this.options.cache?.layout.units[s.data[(id) * LABEL_STRIDE + LabelField.Matched]]; + if (previous?.kind === kind && previous.key === key) { + s.data[(id) * LABEL_STRIDE + LabelField.Matched]++; + const marker = this.markers.get(s.data[(id) * LABEL_STRIDE + LabelField.Matched]); + if (marker !== undefined) { s.data[(id) * LABEL_STRIDE + LabelField.CachedTokens] = marker; s.data[(id) * LABEL_STRIDE + LabelField.CachedUnits] = s.data[(id) * LABEL_STRIDE + LabelField.Matched]; } + } else s.data[(id) * LABEL_STRIDE + LabelField.Intact] = 0; + } + } + + private flush(id: number, before: number): void { + const s = this.s; + if (!this.buffered || !s.data[(id) * LABEL_STRIDE + LabelField.Intact] || s.pending[id].length === 0) return; + const ordered = [...s.pending[id]].sort((a, b) => a.sequence - b.sequence || a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key)); + let consumed = 0; + while (consumed < ordered.length && ordered[consumed].sequence < before) { + const emission = ordered[consumed++]; + const previous = this.options.cache?.layout.units[s.data[(id) * LABEL_STRIDE + LabelField.Matched]]; + if (previous?.kind === emission.kind && previous.key === emission.key) { + s.data[(id) * LABEL_STRIDE + LabelField.Matched]++; + const marker = this.markers.get(s.data[(id) * LABEL_STRIDE + LabelField.Matched]); + if (marker !== undefined) { s.data[(id) * LABEL_STRIDE + LabelField.CachedTokens] = marker; s.data[(id) * LABEL_STRIDE + LabelField.CachedUnits] = s.data[(id) * LABEL_STRIDE + LabelField.Matched]; } + } else { s.data[(id) * LABEL_STRIDE + LabelField.Intact] = 0; s.pending[id] = []; return; } + } + s.pending[id] = ordered.slice(consumed); + } + + private action(ids: readonly string[], level: number, summaryId?: string): Action { + let fidelity = 0, continuity = 0, tokens = 0, extension = 0, sequence = Infinity; + const presentation = this.options.presentation; + for (const id of ids) { + const chunk = this.chunks.get(id)!; + sequence = Math.min(sequence, chunk.sequence); + if (level > 0) fidelity += fidelityLeafLoss(chunk, level, this.newest, this.policy); + continuity += continuityLeafLoss(chunk, level, level === 0 ? `raw:${id}` : `summary:${summaryId}`, + presentation?.leaves.get(id), presentation?.currentSeq ?? 0, this.age.get(id)!, this.policy); + if (level === 0) { + tokens += chunk.rawTokens; + if (presentation && !presentation.leaves.has(id)) extension += chunk.rawTokens; + } + } + if (summaryId) { + const summary = this.forest.summary(summaryId)!; + tokens = summary.recallTokens; + if (presentation && summary.leafIds.length > 0 && summary.leafIds.every((id) => !presentation.leaves.has(id))) extension = tokens; + } + return { trace: ids.length ? this.s.traces.action(ids, level) : 0, ids, level, fidelity, continuity, tokens, extension, key: summaryId, sequence }; + } + + private apply(id: number, action: Action): void { + const s = this.s; + if (!action.trace) return; + s.assign(id, action.trace, action.fidelity, action.continuity); + if (!s.data[(id) * LABEL_STRIDE + LabelField.Intact]) { s.data[(id) * LABEL_STRIDE + LabelField.Tokens] += action.tokens; s.data[(id) * LABEL_STRIDE + LabelField.Extension] += action.extension; return; } + if (action.key) this.emit(id, 'recall', action.key, action.tokens, action.extension > 0, action.sequence); + else for (const leafId of action.ids) { + const leaf = this.forest.leaf(leafId)!; + this.emit(id, 'raw', leafId, leaf.rawTokens, + this.options.presentation !== undefined && !this.options.presentation.leaves.has(leafId), leaf.sequence); + } + } + + private children(summaryId: string, incoming: number[], active: ReadonlySet | undefined, limit: number): number[] { + const summary = this.forest.summary(summaryId)!; + const children = [ + ...summary.directLeafIds.map((id) => ({ kind: 'leaf' as const, id, sequence: this.forest.leaf(id)!.sequence })), + ...summary.childSummaryIds.map((id) => ({ kind: 'summary' as const, id, sequence: this.forest.summary(id)!.firstSequence })), + ].sort((a, b) => a.sequence - b.sequence || a.id.localeCompare(b.id)).filter((child) => !active || + (child.kind === 'leaf' ? active.has(child.id) : this.forest.summary(child.id)!.leafIds.some((id) => active.has(id)))); + let labels = incoming; + for (let at = 0; at < children.length;) { + const child = children[at]; + if (this.cacheRelevant && this.buffered) for (const id of labels) this.flush(id, Math.min(child.sequence, limit)); + if (child.kind === 'summary') { + labels = this.summary(child.id, labels, active, Math.min(limit, children[at + 1]?.sequence ?? Infinity)); + at++; continue; + } + const ids: string[] = []; + while (at < children.length && children[at].kind === 'leaf') { + const id = children[at++].id; + if (!this.forest.leaf(id)!.externallyAccounted) ids.push(id); + } + if (ids.some((id) => !this.forest.leaf(id)!.allowedLevels.includes(0))) { + for (const id of labels) this.s.release(id); + labels = []; + } else { + const action = this.action(ids, 0); + for (const id of labels) this.apply(id, action); + } + labels = this.prune(labels); + } + return labels; + } + + private summary(summaryId: string, incoming: number[], active: ReadonlySet | undefined, limit: number): number[] { + this.expanded += incoming.length; + const summary = this.forest.summary(summaryId)!; + const live = summary.leafIds.filter((id) => (!active || active.has(id)) && !this.forest.leaf(id)!.externallyAccounted); + const participants = live.filter((id) => this.forest.leaf(id)!.allowedLevels.includes(summary.level)); + const holes = live.filter((id) => !this.forest.leaf(id)!.allowedLevels.includes(summary.level)); + let selected: number[] = []; + if (participants.length) { + const action = this.action(participants, summary.level, summaryId); + selected = incoming.map((id) => { const copy = this.s.clone(id); this.apply(copy, action); return copy; }); + if (holes.length) selected = this.children(summaryId, selected, new Set(holes), limit); + } + const expanded = this.children(summaryId, incoming, active, limit); + return this.prune(selected.concat(expanded)); + } + + solve(): ParetoPolicySolveResult { + const feasibility = this.knownFeasibility; + const s = this.s; + const initial = s.initial(this.cacheRelevant, this.leaves.filter((leaf) => leaf.externallyAccounted).map((leaf) => leaf.id)); + if (this.inputs.headTokens > 0) this.emit(initial, 'head', 'head', this.inputs.headTokens, false); + let labels = [initial]; + for (let at = 0; at < this.forest.roots.length; at++) { + const root = this.forest.roots[at]; + if (this.cacheRelevant && this.buffered) for (const id of labels) this.flush(id, root.firstSequence); + if (root.kind === 'summary') labels = this.summary(root.id, labels, undefined, this.forest.roots[at + 1]?.firstSequence ?? Infinity); + else { + const leaf = this.forest.leaf(root.id)!; + if (!leaf.externallyAccounted) { + const action = this.action([leaf.id], 0); + for (const id of labels) this.apply(id, action); + } + labels = this.prune(labels); + } + } + this.progress('propagated', labels.length); + const evaluator = new TerminalPolicyEvaluator(this.inputs, this.forest, this.options); + const terminal: Array<{ trace: FrontierTraceReference; tokens: number }> = []; + const error: ApproximationEnvelope = { token: 0, continuity: 0, fidelity: 0, cache: 0 }; + for (const id of labels) { + error.token = Math.max(error.token, s.data[(id) * LABEL_STRIDE + LabelField.TokenError]); error.continuity = Math.max(error.continuity, s.data[(id) * LABEL_STRIDE + LabelField.ContinuityError]); + error.fidelity = Math.max(error.fidelity, s.data[(id) * LABEL_STRIDE + LabelField.FidelityError]); error.cache = Math.max(error.cache, s.data[(id) * LABEL_STRIDE + LabelField.CacheError]); + this.flush(id, Infinity); + if (this.inputs.tailTokens > 0) this.emit(id, 'tail', 'tail', this.inputs.tailTokens, false); + if (s.data[(id) * LABEL_STRIDE + LabelField.Tokens] <= this.options.maxTokens) terminal.push({ + trace: s.traces.reference(s.finishTrace(id)), tokens: s.data[(id) * LABEL_STRIDE + LabelField.Tokens], + }); + s.release(id); + } + const roundoff = 32 * Number.EPSILON * this.leaves.length * Math.max(1, this.options.maxTokens); + if (!terminal.some((candidate) => { + if (Math.abs(candidate.tokens - feasibility.floorTokens) > roundoff) return false; + const prepared = evaluator.candidate(candidate.trace, candidate.tokens); + return [...feasibility.frontier].every(([id, level]) => prepared.frontier.get(id) === level); + })) { + const byLevel = new Map(); + for (const [id, level] of feasibility.frontier) { const ids = byLevel.get(level); if (ids) ids.push(id); else byLevel.set(level, [id]); } + let trace = 0; + for (const [level, ids] of byLevel) trace = s.traces.append(trace, s.traces.action(ids, level)); + terminal.push({ trace: s.traces.reference(trace), tokens: feasibility.floorTokens }); + } + const stats = { + statesVisited: this.states, candidatesGenerated: this.created, maxCandidatesAtState: this.maximum, terminalCandidates: terminal.length, + }; + const full = this.options.terminalEvaluation === 'full'; + const candidates = full ? terminal.map((candidate) => evaluator.candidate(candidate.trace, candidate.tokens)) : undefined; + const bounded = full ? undefined : terminal.map((candidate) => evaluator.estimate(candidate.trace, candidate.tokens)); + this.progress('evaluated', terminal.length); + const result = full + ? new ExactKvUnifiedPolicySolver(this.inputs, this.forest).scorePreparedCandidates(candidates!, this.options, stats, this.cacheRelevant) + : scoreBoundedCandidates(bounded!, this.options, stats, this.cacheRelevant, evaluator.orderedLeafIds); + this.progress('scored', terminal.length); + const propagation = { + labelsCreated: this.created, labelsExpanded: this.expanded, labelsDominated: this.dominated, + states: this.states, maxLabelsPerState: this.maximum, terminalLabels: terminal.length, + tokenBucketSize: this.tBucket, continuityBucketSize: this.kBucket, fidelityBucketSize: this.fBucket, + approximationBounded: true, approximationScoreErrorBound: this.approximationBound(error), + approximationTokenErrorBound: error.token, approximationContinuityErrorBound: error.continuity, + approximationFidelityErrorBound: error.fidelity, approximationCacheErrorBound: error.cache, + storageMode: 'packed' as const, packedLabelSlots: s.slots, packedTraceNodes: s.traces.nodes, + terminalEvaluationMode: full ? 'full' as const : 'selective' as const, + exactTerminalEvaluations: evaluator.exactEvaluations, + }; + // Do not spread result: its candidate-list getter deliberately performs + // the remaining exact work only when an observer requests that list. + return { + feasible: true, selected: result.selected, cacheFloor: result.cacheFloor, + continuityFloor: result.continuityFloor, cacheRelevant: result.cacheRelevant, + enumeration: result.enumeration, propagation, + get candidates() { return result.candidates; }, + }; + } +} diff --git a/src/adaptive/kv-unified-pareto.ts b/src/adaptive/kv-unified-pareto.ts index cc0d24bd..f9c32979 100644 --- a/src/adaptive/kv-unified-pareto.ts +++ b/src/adaptive/kv-unified-pareto.ts @@ -2,6 +2,7 @@ import type { ChunkId } from './folding-strategy.js'; import type { PickerInputs } from './picker.js'; import { certifyCarriedLayout, type HysteresisCertificate } from './kv-unified-certificate.js'; import { TerminalPolicyEvaluator } from './kv-unified-terminal.js'; +import { PackedDagSolver } from './kv-unified-packed.js'; import { CanonicalSummaryForest, SparseLabelCeilingError, @@ -51,7 +52,7 @@ interface ParetoLabel { approximation: ApproximationEnvelope; } -interface ApproximationEnvelope { +export interface ApproximationEnvelope { token: number; continuity: number; fidelity: number; @@ -67,6 +68,11 @@ interface AssignmentTrace { } export interface ParetoPropagationStats { + readonly storageMode?: 'packed' | 'objects'; + readonly packedLabelSlots?: number; + readonly packedTraceNodes?: number; + readonly terminalEvaluationMode?: 'full' | 'selective'; + readonly exactTerminalEvaluations?: number; readonly labelsCreated: number; readonly labelsExpanded: number; readonly labelsDominated: number; @@ -99,6 +105,10 @@ export type ParetoSolveOptions = ExactPolicySolveOptions & { engine?: 'auto' | 'leaf' | 'dag'; /** Opt-in prototype: prove the hysteresis selection before propagating labels. */ hysteresisCertificate?: boolean; + /** Diagnostic reference path; the normal DAG path uses packed storage. */ + storage?: 'packed' | 'objects'; + /** Diagnostic exhaustive rescoring; selection otherwise uses exact bounds. */ + terminalEvaluation?: 'full' | 'selective'; /** Optional diagnostic observer; never used to choose a cut or stop a solve. */ onProgress?: (event: { phase: string; elapsedMs: number; states: number; labels: number }) => void; }; @@ -139,6 +149,12 @@ export class ParetoKvUnifiedPolicySolver { if (certified) return certified; } if (options.engine !== 'leaf') { + if (options.storage !== 'objects') { + const feasibility = this.forest.minimumTokens(options.maxTokens); + if (!feasibility.feasible) return { feasible: false, feasibility }; + return new PackedDagSolver(this.inputs, this.forest, options, gapBearingOwnership || internalHoles, + (error) => this.approximationBound(options, normalizePolicy(options.policy), error), feasibility).solve(); + } this.bufferGapEmissions = gapBearingOwnership || internalHoles; try { return this.solveDag(options); diff --git a/src/adaptive/kv-unified-policy.ts b/src/adaptive/kv-unified-policy.ts index b3f86088..0524d89b 100644 --- a/src/adaptive/kv-unified-policy.ts +++ b/src/adaptive/kv-unified-policy.ts @@ -231,12 +231,8 @@ export class ExactKvUnifiedPolicySolver { const candidates: ExactPolicyCandidate[] = unscored.map((candidate) => { const cacheExcess = Math.max(0, candidate.cacheChurn - cacheFloor); const continuityExcess = Math.max(0, candidate.continuityLoss - continuityFloor); - const score = - candidate.fidelityLoss + - candidate.budgetPenalty + - quadratic(cacheExcess, policy.cacheScale, policy.cacheLambda) + - continuityMultiplier * - quadratic(continuityExcess, policy.continuityScale, policy.continuityLambda); + const score = policyScore(candidate.fidelityLoss, candidate.budgetPenalty, + candidate.cacheChurn, candidate.continuityLoss, cacheFloor, continuityFloor, policy, continuityMultiplier); const result = { get frontier() { return candidate.frontier; }, get layout() { return candidate.layout; }, @@ -248,13 +244,8 @@ export class ExactKvUnifiedPolicySolver { matching.set(result, candidate.matchesPresentation); return result; }); - candidates.sort((a, b) => - a.score - b.score || - a.renderedTokens - b.renderedTokens || - frontierSignature(a.frontier, this.orderedChunks.map((chunk) => chunk.id)).localeCompare( - frontierSignature(b.frontier, this.orderedChunks.map((chunk) => chunk.id)), - ), - ); + const leafIds = this.orderedChunks.map((chunk) => chunk.id); + candidates.sort((a, b) => comparePolicyCandidates(a, b, leafIds)); let selected = candidates[0]; const epsilon = Number.isFinite(options.adoptEpsilon) && (options.adoptEpsilon ?? 0) > 0 @@ -488,13 +479,25 @@ function clamp(value: number, low: number, high: number): number { return Math.min(high, Math.max(low, value)); } -function normalizeContinuityMultiplier(value: number | undefined): number { +export function normalizeContinuityMultiplier(value: number | undefined): number { if (value === undefined) return 1; // Malformed relaxation fails closed: it must never make continuity cheaper. if (!Number.isFinite(value) || value < 0 || value > 1) return 1; return value; } +export function comparePolicyCandidates(a: ExactPolicyCandidate, b: ExactPolicyCandidate, + leafIds: readonly ChunkId[]): number { + return a.score - b.score || a.renderedTokens - b.renderedTokens || + frontierSignature(a.frontier, leafIds).localeCompare(frontierSignature(b.frontier, leafIds)); +} + +export function policyScore(fidelity: number, budget: number, cache: number, continuity: number, + cacheFloor: number, continuityFloor: number, policy: KvUnifiedWelfarePolicy, multiplier: number): number { + return fidelity + budget + quadratic(Math.max(0, cache - cacheFloor), policy.cacheScale, policy.cacheLambda) + + multiplier * quadratic(Math.max(0, continuity - continuityFloor), policy.continuityScale, policy.continuityLambda); +} + function frontierSignature( frontier: ReadonlyMap, leafIds: readonly ChunkId[], diff --git a/src/adaptive/kv-unified-selective.ts b/src/adaptive/kv-unified-selective.ts new file mode 100644 index 00000000..2460d844 --- /dev/null +++ b/src/adaptive/kv-unified-selective.ts @@ -0,0 +1,135 @@ +import type { ChunkId } from './folding-strategy.js'; +import type { ExactCutEnumerationStats } from './kv-unified.js'; +import { + comparePolicyCandidates, normalizeContinuityMultiplier, normalizePolicy, policyScore, + type ExactPolicyCandidate, type ExactPolicySolveOptions, type ExactPolicySolveResult, type UnscoredCandidate, +} from './kv-unified-policy.js'; + +export interface MetricInterval { readonly lower: number; readonly upper: number } +export interface BoundedPolicyCandidate { + readonly renderedTokens: number; + readonly budgetPenalty: number; + readonly fidelity: MetricInterval; + readonly continuity: MetricInterval; + /** Exact chronological cache calculation, not a propagation estimate. */ + readonly cacheChurn: number; + readonly matchesPresentation: boolean; + exact(): UnscoredCandidate; +} + +const bitsBuffer = new ArrayBuffer(8); +const floating = new Float64Array(bitsBuffer); +const bits = new BigUint64Array(bitsBuffer); +function nextUp(value: number): number { + if (Number.isNaN(value) || value === Infinity) return value; + if (value === 0) return Number.MIN_VALUE; + floating[0] = value; + bits[0] += value > 0 ? 1n : -1n; + return floating[0]; +} +function nextDown(value: number): number { return -nextUp(-value); } + +/** Both sums use the same nonnegative binary64 leaf terms. The grouped sum + * performs at most 2n additions and the oracle at most n. 16*epsilon*(n+1) + * exceeds their combined gamma bounds (including the scale conversion) when + * it is below 1/4. Outward rounding and a subnormal allowance widen it further. + * A zero sum of nonnegative floating terms is exactly zero. */ +export function nonnegativeSumInterval(value: number, leaves: number): MetricInterval { + const factor = 16 * Number.EPSILON * (leaves + 1); + if (!Number.isFinite(value) || value < 0 || factor >= 0.25) return { lower: 0, upper: Infinity }; + if (value === 0) return { lower: 0, upper: 0 }; + const error = nextUp(value * factor + (leaves + 1) * Number.MIN_VALUE); + return { lower: Math.max(0, nextDown(value - error)), upper: nextUp(value + error) }; +} + +/** Preserve exact floors, exact winner/hysteresis/ties, and the full public + * candidate list. Only metrics needed for the decision are evaluated now; + * inspecting candidates later performs the remaining exact work lazily. */ +export function scoreBoundedCandidates(source: readonly BoundedPolicyCandidate[], options: ExactPolicySolveOptions, + stats: ExactCutEnumerationStats, cacheRelevant: boolean, leafIds: readonly ChunkId[]): Extract { + if (source.length === 0) throw new Error('kv-unified has no feasible terminal candidates'); + const policy = normalizePolicy(options.policy); + const multiplier = normalizeContinuityMultiplier(options.continuityMultiplier); + const epsilon = options.presentation && Number.isFinite(options.adoptEpsilon) && (options.adoptEpsilon ?? 0) > 0 + ? options.adoptEpsilon! : 0; + const exact: Array = new Array(source.length); + const refine = (index: number) => exact[index] ??= source[index].exact(); + let fullyEvaluate = source.some((candidate) => + !Number.isFinite(candidate.fidelity.upper) || !Number.isFinite(candidate.continuity.upper) || + !Number.isFinite(candidate.cacheChurn) || !Number.isFinite(candidate.budgetPenalty)); + + // Cache churn is exact for every candidate. Continuity gets its own floor + // search, independent of whether a candidate could ever win on welfare. + let cacheFloor = Infinity; + let continuityFloor = Infinity; + if (!fullyEvaluate) { + let witness = 0; + for (let i = 0; i < source.length; i++) { + cacheFloor = Math.min(cacheFloor, source[i].cacheChurn); + if (source[i].continuity.upper < source[witness].continuity.upper) witness = i; + } + continuityFloor = source[witness].continuity.upper === 0 ? 0 : refine(witness).continuityLoss; + for (let i = 0; i < source.length; i++) { + if (source[i].continuity.lower < continuityFloor) continuityFloor = Math.min(continuityFloor, refine(i).continuityLoss); + } + } + + const lower = new Float64Array(source.length); + let upperBest = Infinity, upperMatching = Infinity; + if (!fullyEvaluate) for (let i = 0; i < source.length; i++) { + const c = source[i]; + lower[i] = policyScore(c.fidelity.lower, c.budgetPenalty, c.cacheChurn, c.continuity.lower, + cacheFloor, continuityFloor, policy, multiplier); + const upper = policyScore(c.fidelity.upper, c.budgetPenalty, c.cacheChurn, c.continuity.upper, + cacheFloor, continuityFloor, policy, multiplier); + if (!Number.isFinite(lower[i]) || !Number.isFinite(upper)) { fullyEvaluate = true; break; } + upperBest = Math.min(upperBest, upper); + if (epsilon > 0 && c.matchesPresentation) upperMatching = Math.min(upperMatching, upper); + } + if (fullyEvaluate) { + cacheFloor = Infinity; continuityFloor = Infinity; + for (let i = 0; i < source.length; i++) { + const c = refine(i); + cacheFloor = Math.min(cacheFloor, c.cacheChurn); + continuityFloor = Math.min(continuityFloor, c.continuityLoss); + } + } + + const scored: Array = new Array(source.length); + const score = (index: number): ExactPolicyCandidate => { + if (scored[index]) return scored[index]!; + const c = refine(index); + return scored[index] = { + get frontier() { return c.frontier; }, get layout() { return c.layout; }, + renderedTokens: c.renderedTokens, fidelityLoss: c.fidelityLoss, continuityLoss: c.continuityLoss, + cacheChurn: c.cacheChurn, budgetPenalty: c.budgetPenalty, + cacheExcess: Math.max(0, c.cacheChurn - cacheFloor), continuityExcess: Math.max(0, c.continuityLoss - continuityFloor), + score: policyScore(c.fidelityLoss, c.budgetPenalty, c.cacheChurn, c.continuityLoss, cacheFloor, continuityFloor, policy, multiplier), + }; + }; + let best: ExactPolicyCandidate | undefined; + let carried: ExactPolicyCandidate | undefined; + let candidates: ExactPolicyCandidate[] | undefined; + if (fullyEvaluate) { + const matching = new Set(source.flatMap((candidate, index) => candidate.matchesPresentation ? [score(index)] : [])); + candidates = source.map((_candidate, index) => score(index)).sort((a, b) => comparePolicyCandidates(a, b, leafIds)); + best = candidates[0]; + if (epsilon > 0) carried = candidates.find((candidate) => matching.has(candidate)); + } + for (let i = 0; !fullyEvaluate && i < source.length; i++) { + const matching = epsilon > 0 && source[i].matchesPresentation; + if (!fullyEvaluate && lower[i] > upperBest && (!matching || lower[i] > upperMatching)) continue; + const c = score(i); + if (!best || comparePolicyCandidates(c, best, leafIds) < 0) best = c; + if (matching && (!carried || comparePolicyCandidates(c, carried, leafIds) < 0)) carried = c; + } + if (!best) throw new Error('kv-unified score intervals failed to retain a winner'); + const selected = carried && carried.score <= best.score + epsilon ? carried : best; + return { + feasible: true, selected, cacheFloor, continuityFloor, cacheRelevant, enumeration: stats, + get candidates() { + return candidates ??= source.map((_candidate, index) => score(index)) + .sort((a, b) => comparePolicyCandidates(a, b, leafIds)); + }, + }; +} diff --git a/src/adaptive/kv-unified-terminal.ts b/src/adaptive/kv-unified-terminal.ts index cb85dfe3..36dfb0d0 100644 --- a/src/adaptive/kv-unified-terminal.ts +++ b/src/adaptive/kv-unified-terminal.ts @@ -6,6 +6,7 @@ import { type ExactPolicySolveOptions, type UnscoredCandidate, } from './kv-unified-policy.js'; import type { RenderLayout, RenderedUnit } from './render-offsets.js'; +import { nonnegativeSumInterval, type BoundedPolicyCandidate } from './kv-unified-selective.js'; export interface FrontierTrace { readonly parent: FrontierTrace | null; @@ -13,6 +14,27 @@ export interface FrontierTrace { readonly level: number; } +export interface FrontierTraceSource { + forEachAssignment(visit: (ids: readonly ChunkId[], level: number) => void): void; +} + +export type FrontierTraceReference = FrontierTrace | FrontierTraceSource | null; + +interface CompiledAction { + readonly fidelity: number; + readonly continuity: number; + readonly matches: boolean; + readonly leaves: number; + /** Pairs: emitted unit code, earliest assigned leaf index. */ + readonly emissions: Uint32Array; +} + +export function visitFrontierTrace(trace: FrontierTraceReference, + visit: (ids: readonly ChunkId[], level: number) => void): void { + if (trace && 'forEachAssignment' in trace) trace.forEachAssignment(visit); + else for (let cursor = trace; cursor; cursor = cursor.parent) visit(cursor.ids, cursor.level); +} + /** Precompute per-leaf terms once, then sum in exactly the oracle's order. * Frontier Maps and rendered layouts remain lazy until a caller needs them. * This removes billions of repeated powers/hash lookups and keeps only one @@ -22,6 +44,7 @@ export class TerminalPolicyEvaluator { private readonly ids: readonly string[]; private readonly index: Map; private readonly ranges = new WeakMap(); + private readonly actions = new WeakMap>(); private readonly stride: number; private readonly fidelity: Float64Array; private readonly continuity: Float64Array; @@ -33,17 +56,23 @@ export class TerminalPolicyEvaluator { private readonly unitKinds: RenderedUnit['kind'][]; private readonly unitExtension: Uint8Array; private readonly emitted: Uint32Array; + private readonly firstEmission: Uint32Array; + private readonly orderedEmissions: Uint32Array; + private readonly emissionAtLeaf: Uint32Array; private readonly previousUnits: Uint32Array; private readonly markerUnits: ReadonlySet; private readonly headCode: number; private readonly tailCode: number; private generation = 0; private readonly policy; + private readonly maxTokens: number; + private evaluations = 0; readonly cacheRelevant: boolean; constructor(private readonly inputs: PickerInputs, private readonly forest: CanonicalSummaryForest, private readonly options: ExactPolicySolveOptions) { this.policy = normalizePolicy(options.policy); + this.maxTokens = options.maxTokens; this.chunks = [...inputs.chunks].sort((a, b) => a.sequence - b.sequence || a.id.localeCompare(b.id)); this.ids = this.chunks.map((chunk) => chunk.id); this.index = new Map(this.chunks.map((chunk, index) => [chunk.id, index])); @@ -62,6 +91,9 @@ export class TerminalPolicyEvaluator { this.unitKinds = new Array(this.tailCode + 1); this.unitExtension = new Uint8Array(this.tailCode + 1); this.emitted = new Uint32Array(this.tailCode + 1); + this.firstEmission = new Uint32Array(this.tailCode + 1); + this.orderedEmissions = new Uint32Array(this.tailCode + 1); + this.emissionAtLeaf = new Uint32Array(this.chunks.length); const presentation = options.presentation; for (const summary of summaries) { const code = summaryCode.get(summary.id)!; @@ -108,27 +140,119 @@ export class TerminalPolicyEvaluator { } } - private fill(trace: FrontierTrace | null): void { + get orderedLeafIds(): readonly string[] { return this.ids; } + get exactEvaluations(): number { return this.evaluations; } + + private compileAction(ids: readonly string[], level: number): CompiledAction { + let levels = this.actions.get(ids); + const cached = levels?.get(level); + if (cached) return cached; + let fidelity = 0, continuity = 0, matches = true; + const units = new Map(); + for (const id of ids) { + const index = this.index.get(id)!; + const at = index * this.stride + level; + fidelity += this.fidelity[at]; continuity += this.continuity[at]; + matches &&= this.matches[at] === 1; + const code = this.representations[at]; + if (code) units.set(code, Math.min(units.get(code) ?? Infinity, index)); + } + const emissions = new Uint32Array(units.size * 2); + let cursor = 0; + for (const [code, index] of units) { emissions[cursor++] = code; emissions[cursor++] = index; } + const result = { fidelity, continuity, matches, leaves: ids.length, emissions }; + if (!levels) { levels = new Map(); this.actions.set(ids, levels); } + levels.set(level, result); + return result; + } + + /** Sum compiled action terms instead of scanning every leaf for every cut. + * Cache is exact: deduplicate emitted units, sort their first assigned leaf + * indices, and perform the same chronological offset/extension arithmetic. + * No cache-prefix estimate from propagation is trusted here. */ + estimate(trace: FrontierTraceReference, renderedTokens: number): BoundedPolicyCandidate { + let exact: UnscoredCandidate | undefined; + const refine = () => exact ??= this.candidate(trace, renderedTokens); + let fidelity = 0, continuity = 0, matches = true, leaves = 0, unitCount = 0; + const generation = ++this.generation; + visitFrontierTrace(trace, (ids, level) => { + const action = this.compileAction(ids, level); + fidelity += action.fidelity; continuity += action.continuity; + matches &&= action.matches; leaves += action.leaves; + if (!this.cacheRelevant) return; + for (let i = 0; i < action.emissions.length; i += 2) { + const code = action.emissions[i], index = action.emissions[i + 1]; + if (this.emitted[code] !== generation) { + this.emitted[code] = generation; this.firstEmission[code] = index; + this.orderedEmissions[unitCount++] = code; + } else this.firstEmission[code] = Math.min(this.firstEmission[code], index); + } + }); + // Internal solver traces are complete disjoint cuts. Partial diagnostic + // traces keep the old default-raw behavior by using the exact evaluator. + if (leaves !== this.ids.length || !Number.isFinite(fidelity) || !Number.isFinite(continuity)) { + const c = refine(); + return { renderedTokens, budgetPenalty: c.budgetPenalty, cacheChurn: c.cacheChurn, + fidelity: { lower: c.fidelityLoss, upper: c.fidelityLoss }, + continuity: { lower: c.continuityLoss, upper: c.continuityLoss }, + matchesPresentation: c.matchesPresentation!, exact: refine }; + } + let cacheChurn = 0; + if (this.cacheRelevant) { + // Convert to first-leaf indices so TypedArray's native numeric sort can + // be used without a JS comparator or per-emission objects. + for (let i = 0; i < unitCount; i++) { + const code = this.orderedEmissions[i], index = this.firstEmission[code]; + this.emissionAtLeaf[index] = code; this.orderedEmissions[i] = index; + } + const ordered = this.orderedEmissions.subarray(0, unitCount); + ordered.sort(); + let offset = 0, cached = 0, prefix = 0, extension = 0; + let intact = true; + const emit = (code: number) => { + const tokens = this.unitTokens[code]; offset += tokens; + if (this.unitExtension[code]) extension += tokens; + if (intact) { + if (this.previousUnits[prefix] === code) { + prefix++; + if (this.markerUnits.has(prefix)) cached = offset; + } else intact = false; + } + }; + if (this.unitTokens[this.headCode] > 0) emit(this.headCode); + for (const index of ordered) emit(this.emissionAtLeaf[index]); + if (this.unitTokens[this.tailCode] > 0) emit(this.tailCode); + cacheChurn = Math.max(0, offset - cached - extension) * Math.max(0, this.policy.cacheWritePrice - this.policy.cacheReadPrice); + } + return { + renderedTokens, budgetPenalty: budgetPenalty(renderedTokens, this.maxTokens, this.policy), + cacheChurn, fidelity: nonnegativeSumInterval(fidelity, leaves), continuity: nonnegativeSumInterval(continuity, leaves), + matchesPresentation: matches, exact: refine, + }; + } + + private fill(trace: FrontierTraceReference): void { this.levels.fill(0); // Valid cut traces assign each leaf exactly once, so traversal direction // is immaterial. Reuse numeric indices for shared summary/raw-run actions. - for (let cursor = trace; cursor; cursor = cursor.parent) { - let ranges = this.ranges.get(cursor.ids); + visitFrontierTrace(trace, (ids, level) => { + let ranges = this.ranges.get(ids); if (!ranges) { - const indices = cursor.ids.map((id) => this.index.get(id)!).sort((a, b) => a - b); + const indices = ids.map((id) => this.index.get(id)!).sort((a, b) => a - b); const spans: number[] = []; for (const index of indices) { if (spans.length > 0 && spans[spans.length - 1] === index) spans[spans.length - 1]++; else spans.push(index, index + 1); } ranges = Uint32Array.from(spans); - this.ranges.set(cursor.ids, ranges); + this.ranges.set(ids, ranges); } - for (let i = 0; i < ranges.length; i += 2) this.levels.fill(cursor.level, ranges[i], ranges[i + 1]); - } + for (let i = 0; i < ranges.length; i += 2) this.levels.fill(level, ranges[i], ranges[i + 1]); + }); } - candidate(trace: FrontierTrace | null, renderedTokens: number): UnscoredCandidate { + candidate(trace: FrontierTraceReference, renderedTokens: number): UnscoredCandidate { + this.evaluations++; this.fill(trace); let fidelityLoss = 0; let continuityLoss = 0; @@ -207,7 +331,7 @@ export class TerminalPolicyEvaluator { get frontier() { return getFrontier(); }, get layout() { return getLayout(); }, fidelityLoss, continuityLoss, renderedTokens, cacheChurn, matchesPresentation, - budgetPenalty: budgetPenalty(renderedTokens, this.options.maxTokens, this.policy), + budgetPenalty: budgetPenalty(renderedTokens, this.maxTokens, this.policy), }; } } diff --git a/test/adaptive/kv-unified-packed.test.ts b/test/adaptive/kv-unified-packed.test.ts new file mode 100644 index 00000000..94caa2c1 --- /dev/null +++ b/test/adaptive/kv-unified-packed.test.ts @@ -0,0 +1,142 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { CanonicalSummaryForest } from '../../src/adaptive/kv-unified.js'; +import { ParetoKvUnifiedPolicySolver } from '../../src/adaptive/kv-unified-pareto.js'; +import { TerminalPolicyEvaluator, type FrontierTrace } from '../../src/adaptive/kv-unified-terminal.js'; +import { renderLayout } from '../../src/adaptive/render-offsets.js'; +import { SummaryTree } from '../../src/adaptive/summary-tree.js'; +import type { PickerInputs } from '../../src/adaptive/picker.js'; +import { MockChronicle, buildChronicleWithChain } from './harness.js'; +import { PackedLabels, PackedBuckets, LABEL_STRIDE, LabelField } from '../../src/adaptive/kv-unified-packed-storage.js'; + +test('packed label growth, reuse, and branching preserve immutable trace ancestry', () => { + const labels = new PackedLabels(); + const root = labels.initial(true, ['external']); + labels.data[root * LABEL_STRIDE + LabelField.Tokens] = 123.5; + const action = labels.traces.action(['a'], 1); + labels.assign(root, action, 1.5, 2.25); + const copies = Array.from({ length: 3000 }, () => labels.clone(root)); + const trace = labels.traces.reference(labels.finishTrace(copies[0])); + for (const id of copies) { + assert.equal(labels.data[id * LABEL_STRIDE + LabelField.Tokens], 123.5); + assert.equal(labels.data[id * LABEL_STRIDE + LabelField.Continuity], 2.25); + labels.release(id); + } + const reused = labels.clone(root); + labels.assign(reused, labels.traces.action(['b'], 2), 9, 7); + const original: Array<[readonly string[], number]> = []; + trace.forEachAssignment((ids, level) => original.push([ids, level])); + assert.deepEqual(original, [[['a'], 1], [['external'], 0]]); + const fork: Array<[readonly string[], number]> = []; + labels.traces.reference(labels.finishTrace(reused)).forEachAssignment((ids, level) => fork.push([ids, level])); + assert.deepEqual(fork, [[['b'], 2], [['a'], 1], [['external'], 0]]); + assert.equal(labels.slots, 3001, 'freed slots are reused'); +}); + +test('reusable buckets keep numeric/string identities separate and reset each generation', () => { + const buckets = new PackedBuckets(100); + for (let pass = 0; pass < 100; pass++) { + buckets.begin(2500); + assert.equal(buckets.group(17), 0); + assert.equal(buckets.group('17'), 1); + assert.equal(buckets.group(17), 0); + assert.equal(buckets.group(-1), 2); + for (let i = 1000; i < 3500; i++) buckets.group(i); + assert.equal(buckets.count, 2503); + } +}); + +test('delayed exact evaluation retains captured budgets and partial traces retain default-raw semantics', () => { + const chronicle = buildChronicleWithChain({ chunkCount: 4, tokensPerChunk: 90, mergeThreshold: 2, recallPairTokens: 55 }); + const inputs: PickerInputs = { chunks: chronicle.chunks, summaries: chronicle.summaries, + recallPairTokens: chronicle.recallPairTokens, headTokens: 0, tailTokens: 0, + headChunkIds: new Set(), tailChunkIds: new Set() }; + const forest = new CanonicalSummaryForest(inputs); + const options = { maxTokens: 400 }; + const evaluator = new TerminalPolicyEvaluator(inputs, forest, options); + let trace: FrontierTrace | null = null; + for (const chunk of inputs.chunks) trace = { parent: trace, ids: [chunk.id], level: 0 }; + const expected = evaluator.candidate(trace, 360); + const estimated = evaluator.estimate(trace, 360); + options.maxTokens = 100; + for (const chunk of inputs.chunks) chunk.rawTokens *= 10; + assert.deepEqual(estimated.exact(), expected); + const partial = evaluator.estimate(null, 360); + assert.deepEqual(partial.exact(), expected); +}); + +test('packed/full and packed/selective agree with object storage, including every retained candidate and error envelope', () => { + let state = 73418; + const random = () => ((state = (Math.imul(state, 1664525) + 1013904223) >>> 0) / 2 ** 32); + for (let run = 0; run < 90; run++) { + const chronicle = run % 3 === 0 ? new MockChronicle({ recallPairTokens: 30 }) : + buildChronicleWithChain({ chunkCount: 8, tokensPerChunk: 70, mergeThreshold: 2, recallPairTokens: 30 }); + if (run % 3 === 0) { + for (let i = 0; i < 9; i++) chronicle.addChunk({ id: `c${i}`, rawTokens: 70 }); + const a = chronicle.produceL1(['c0', 'c3', 'c6']); + const b = chronicle.produceL1(['c1', 'c4', 'c7']); + chronicle.produceL1(['c2', 'c5', 'c8']); + chronicle.produceUpper(2, [a.id, b.id]); + } + for (const chunk of chronicle.chunks) { + chunk.rawTokens = 10 + Math.floor(random() * 200) + (run % 4 === 0 ? random() : 0); + chunk.salience = random(); + } + for (const [id] of chronicle.recallPairTokens) chronicle.recallPairTokens.set(id, 10 + random() * 50); + const inputs: PickerInputs = { chunks: chronicle.chunks, summaries: chronicle.summaries, + recallPairTokens: chronicle.recallPairTokens, headTokens: run % 7, tailTokens: run % 13, + headChunkIds: new Set(), tailChunkIds: new Set() }; + const before = new CanonicalSummaryForest(inputs, { preserveGapBearingSummaries: true }); + const cuts = before.enumerateExactCuts().candidates; + const previousCut = cuts[Math.floor(random() * cuts.length)]; + const layout = renderLayout(inputs, new SummaryTree(inputs), previousCut.frontier); + const previous = new Map(inputs.chunks.slice(0, run % 2 ? inputs.chunks.length : Math.floor(inputs.chunks.length / 2)).map((chunk) => { + const level = previousCut.frontier.get(chunk.id) ?? 0; + const id = before.leaf(chunk.id)!.summaryIds.find((id) => before.summary(id)!.level === level); + return [chunk.id, { level, repHash: level === 0 ? `raw:${chunk.id}` : `summary:${id}`, lastChangedSeq: run % 3 }]; + })); + if (run % 4 === 0) inputs.chunks[2].pinned = true; + if (run % 5 === 0) { (inputs.headChunkIds as Set).add(inputs.chunks[0].id); inputs.headTokens += 100; } + const forest = new CanonicalSummaryForest(inputs, { preserveGapBearingSummaries: true }); + const floor = forest.minimumTokens(); + assert.ok(floor.feasible); + const options = { + maxTokens: floor.floorTokens + 1 + random() * 500, adoptEpsilon: run % 2 ? 20 : 0, + presentation: { currentSeq: 7, leaves: previous }, + cache: { immutablePrefixHash: 'same', layout, markers: [{ unitIndex: Math.max(1, Math.floor(layout.units.length / 2)), offset: 100 }, + { unitIndex: layout.units.length, offset: layout.totalTokens }] }, + currentImmutablePrefixHash: run % 6 ? 'same' : 'changed', + tokenBucketSize: run % 7 ? 50 : 0, continuityBucketSize: run % 7 ? 30 : 0, fidelityBucketSize: run % 7 ? 100 : 0, + policy: { continuityLambda: 50 + run, cacheLambda: 100 + run, continuityScale: 100, cacheScale: 100 }, + }; + const solver = new ParetoKvUnifiedPolicySolver(inputs, forest); + const reference = solver.solve({ ...options, storage: 'objects' }); + assert.ok(reference.feasible); + for (const terminalEvaluation of ['full', 'selective'] as const) { + const packed = solver.solve({ ...options, terminalEvaluation }); + assert.ok(packed.feasible); + assert.deepEqual(packed.selected, reference.selected, `selected run ${run}/${terminalEvaluation}`); + assert.equal(packed.cacheFloor, reference.cacheFloor); + assert.equal(packed.continuityFloor, reference.continuityFloor); + assert.deepEqual(packed.candidates, reference.candidates, `candidates run ${run}/${terminalEvaluation}`); + for (const [key, value] of Object.entries(reference.propagation!)) { + assert.equal((packed.propagation as unknown as Record)[key], value, `stat ${key} run ${run}`); + } + } + // Check the bounds themselves against every exact candidate, not only the + // eventual winner; also verify chronological cache corrections exactly. + const evaluator = new TerminalPolicyEvaluator(inputs, forest, options); + for (const cut of forest.enumerateExactCuts({ maxTokens: options.maxTokens }).candidates) { + let trace: FrontierTrace | null = null; + const groups = new Map(); + for (const [id, level] of cut.frontier) { const ids = groups.get(level); if (ids) ids.push(id); else groups.set(level, [id]); } + for (const [level, ids] of groups) trace = { parent: trace, ids, level }; + const estimate = evaluator.estimate(trace, cut.renderedTokens); + const exact = estimate.exact(); + assert.ok(estimate.fidelity.lower <= exact.fidelityLoss && exact.fidelityLoss <= estimate.fidelity.upper); + assert.ok(estimate.continuity.lower <= exact.continuityLoss && exact.continuityLoss <= estimate.continuity.upper); + assert.equal(estimate.cacheChurn, exact.cacheChurn); + assert.equal(estimate.matchesPresentation, exact.matchesPresentation); + } + } +}); diff --git a/test/adaptive/kv-unified-selective.test.ts b/test/adaptive/kv-unified-selective.test.ts new file mode 100644 index 00000000..84cee097 --- /dev/null +++ b/test/adaptive/kv-unified-selective.test.ts @@ -0,0 +1,85 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { nonnegativeSumInterval, scoreBoundedCandidates, type BoundedPolicyCandidate } from '../../src/adaptive/kv-unified-selective.js'; +import { ExactKvUnifiedPolicySolver, type ExactPolicySolveOptions, type UnscoredCandidate } from '../../src/adaptive/kv-unified-policy.js'; +import type { PickerInputs } from '../../src/adaptive/picker.js'; + +const inputs: PickerInputs = { chunks: [{ id: 'x', sequence: 0, rawTokens: 100, currentResolution: 0, + lockedByAgent: false, pinned: false }], summaries: new Map(), headTokens: 0, tailTokens: 0, + headChunkIds: new Set(), tailChunkIds: new Set() }; +const stats = { statesVisited: 0, candidatesGenerated: 0, maxCandidatesAtState: 0, terminalCandidates: 0 }; + +function row(index: number, fidelity: number, continuity = 0, cache = 0, matches = false): UnscoredCandidate { + return { frontier: new Map([['x', index]]), layout: { units: [], totalTokens: 100 }, renderedTokens: 100, + fidelityLoss: fidelity, continuityLoss: continuity, cacheChurn: cache, budgetPenalty: 0, matchesPresentation: matches }; +} +function bounds(value: UnscoredCandidate, refined: Set, slack = 1e-8): BoundedPolicyCandidate { + return { renderedTokens: value.renderedTokens, budgetPenalty: value.budgetPenalty, cacheChurn: value.cacheChurn, + fidelity: { lower: Math.max(0, value.fidelityLoss - slack), upper: value.fidelityLoss + slack }, + continuity: value.continuityLoss === 0 ? { lower: 0, upper: 0 } : + { lower: Math.max(0, value.continuityLoss - slack), upper: value.continuityLoss + slack }, + matchesPresentation: value.matchesPresentation!, exact: () => { refined.add(value); return value; } }; +} + +test('selective scoring refines the winner and carried contender, preserving the complete lazy candidate API', () => { + const values = Array.from({ length: 2000 }, (_, index) => row(index, index, 0, 0, index === 1)); + const refined = new Set(); + const options = { maxTokens: 200, adoptEpsilon: 2, presentation: { currentSeq: 1, leaves: new Map() } }; + const result = scoreBoundedCandidates(values.map((value) => bounds(value, refined)), options, stats, false, ['x']); + assert.equal(result.selected.frontier.get('x'), 1); + assert.equal(refined.size, 2, 'losing candidates need no exact evaluation during selection'); + const reference = new ExactKvUnifiedPolicySolver(inputs).scorePreparedCandidates(values, options, stats, false); + assert.deepEqual(result.selected, reference.selected); + assert.deepEqual(result.candidates, reference.candidates); + assert.equal(refined.size, 2000, 'candidate-list inspection performs deferred exact work'); + assert.ok(result.candidates.includes(result.selected)); +}); + +test('a welfare loser still establishes an exact normalization floor', () => { + const values = [row(0, 10, 10, 5), row(1, 12, 4, 8), row(2, 10000, 2, 0)]; + const refined = new Set(); + const candidates = values.map((value) => bounds(value, refined, 3)); + const options = { maxTokens: 200, policy: { continuityScale: 1, cacheScale: 1, continuityLambda: 1, cacheLambda: 1 } }; + const reference = new ExactKvUnifiedPolicySolver(inputs).scorePreparedCandidates(values, options, stats, true); + const result = scoreBoundedCandidates(candidates, options, stats, true, ['x']); + assert.equal(result.continuityFloor, 2); + assert.equal(result.cacheFloor, 0); + assert.ok(refined.has(values[2])); + assert.equal(result.selected.frontier.get('x'), 1); + assert.equal(result.selected.score, 80); + assert.deepEqual(result.selected, reference.selected); +}); + +test('overlapping bounds, exact ties, and the hysteresis boundary follow the original comparison rule', () => { + const values = [row(2, 1), row(1, 1), row(0, 3, 0, 0, true)]; + for (const adoptEpsilon of [0, 2, 2 - Number.EPSILON, NaN]) { + const options: ExactPolicySolveOptions = { maxTokens: 200, adoptEpsilon, presentation: { currentSeq: 1, leaves: new Map() } }; + const reference = new ExactKvUnifiedPolicySolver(inputs).scorePreparedCandidates(values, options, stats, false); + const result = scoreBoundedCandidates(values.map((value) => bounds(value, new Set(), 10)), options, stats, false, ['x']); + assert.deepEqual(result.selected, reference.selected); + if (adoptEpsilon === 0 || Number.isNaN(adoptEpsilon)) assert.equal(result.selected.frontier.get('x'), 1); + } +}); + +test('positive-sum intervals cover differently grouped sums across binary64 magnitudes', () => { + for (const scale of [Number.MIN_VALUE, 1e-250, 1, 1e100, 1e250]) { + const terms = Array.from({ length: 2000 }, (_, index) => scale * (index % 13 + 1)); + const forward = terms.reduce((sum, value) => sum + value, 0); + let grouped = 0; + for (let i = terms.length; i > 0; i -= 17) grouped += terms.slice(Math.max(0, i - 17), i).reduce((sum, value) => sum + value, 0); + const bound = nonnegativeSumInterval(grouped, terms.length); + assert.ok(bound.lower <= forward && forward <= bound.upper, `scale ${scale}`); + } + assert.deepEqual(nonnegativeSumInterval(0, 100000), { lower: 0, upper: 0 }); +}); + +test('non-finite score arithmetic uses the complete original sort instead of interval decisions', () => { + const values = [row(0, 0, 1e308), row(1, 1), row(2, 2)]; + const options = { maxTokens: 200, continuityMultiplier: 0, policy: { continuityScale: 1 } }; + const refined = new Set(); + const reference = new ExactKvUnifiedPolicySolver(inputs).scorePreparedCandidates(values, options, stats, false); + const result = scoreBoundedCandidates(values.map((value) => bounds(value, refined)), options, stats, false, ['x']); + assert.equal(refined.size, values.length); + assert.deepEqual(result.selected, reference.selected); + assert.deepEqual(result.candidates, reference.candidates); +}); From afb5b4a99eb54eeba5a3c1896f05a86f1c0d948b Mon Sep 17 00:00:00 2001 From: antra-tess Date: Mon, 21 Sep 2026 12:28:22 -0700 Subject: [PATCH 2/2] docs(kv-unified): record packed solver benchmarks and 500-message replay Co-Authored-By: Codex (GPT-6) --- docs/kv-unified-packed-selective.md | 81 ++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/kv-unified-packed-selective.md b/docs/kv-unified-packed-selective.md index 3e39d638..2cfa5b6a 100644 --- a/docs/kv-unified-packed-selective.md +++ b/docs/kv-unified-packed-selective.md @@ -59,7 +59,8 @@ Selection proceeds in three stages: 3. Exactly evaluate every candidate whose lower bound could meet either upper bound. Apply the original score/token/frontier tie-break and hysteresis rule. -The selected candidate, both floors, and its score are exact. Public candidate +Selection, both floors, and scores are exact within the unchanged retained +candidate set; the existing bucket approximation is not removed. Public candidate list inspection performs any remaining exact evaluations and returns the complete sorted list, including the same selected object. Normal solving does not inspect that diagnostic list. Captured budgets and token costs are retained @@ -95,5 +96,83 @@ fidelity/continuity intervals contain the exact result and that cache churn and presentation identity match. Its timing is diagnostic, not the normal selective-scoring timing. +### Large structural fixture + +On the 73,918-message / 3,345-summary fixture, paired fresh-process Bun 1.3.14 runs +against the object backend produced: + +| Case | Object/full | Packed/selective | Reduction | Exact evaluations / retained cuts | +| --- | ---: | ---: | ---: | ---: | +| Warm cache, forced 500k budget | 8.721 s | 5.194 s | 40.4% | 3 / 6,719 | +| Warm cache, newly pinned middle leaf | 8.736 s | 5.229 s | 40.1% | 3 / 6,538 | +| Warm cache, 30k appended raw tokens | 9.149 s | 5.509 s | 39.8% | 3 / 6,648 | + +The evaluation count includes the feasibility-witness check. All selected +frontier hashes, scores, floors, and pre-existing propagation/envelope +statistics match exactly. End-of-run RSS was 1.12–1.13 GB, versus 1.26–1.30 GB +for the object backend; these are process observations, not allocation limits. +An additional diagnostic run checked every one of the 6,538 pinned-case +estimates against its exact evaluation. These are single-run local timings, +not a cross-machine latency guarantee. + +Validation: build and typecheck pass; **822 tests pass / 0 fail**, versus +813 / 0 on the PR #110 parent (9 additional tests). The tests and benchmarks +do not exercise provider calls or a live resident compile. + +For a controlled sequential replay with differential decision checks: + +```sh +bun scripts/replay-kv-unified-messages.mjs --fixture /path/to/solver-inputs.json \ + --metadata /path/to/replay-metadata.json --output /path/to/new-replay \ + --count 500 --verify-every 25 +``` + +This verifies the selected frontier hash, all selected loss terms, both floors, +and score against the object engine at warm-up, every 25th message, and every +layout movement. Reference solves are excluded from measured solve time. It +uses a frozen summary catalogue, fresh atomic-layout cache markers, and accepted +results carried forward, not historical provider/cache-TTL reconstruction. + +### 500-message replay + +Revision `028b065` completed **500 / 500**, with **0 failures** and the +certificate disabled. Timings include canonical forest/solver construction, +solve, and selected frontier materialization. One warm-up is excluded; +percentiles use nearest rank. + +| Statistic | Previous replay | Packed/selective | Reduction | +| --- | ---: | ---: | ---: | +| Minimum | 2.692 s | 1.815 s | 32.6% | +| Mean | 3.122 s | 2.044 s | 34.5% | +| Median | 2.837 s | 1.878 s | 33.8% | +| p90 | 3.544 s | 2.280 s | 35.7% | +| p95 | 4.959 s | 3.145 s | 36.6% | +| p99 | 8.069 s | 4.927 s | 38.9% | +| Maximum | 9.486 s | 5.611 s | 40.9% | + +420 solves were under 2 seconds, 75 were between 2 and 5 seconds, and 5 +were between 5 and 5.62 seconds. The last-100 median was 1.874 seconds. +Only 2–3 exact terminal evaluations were needed per measured solve. + +All **23 reference checks** passed (22 measured samples plus warm-up), +including all four layout-changing solves. Those transitions changed +105 / 1,346 / 1,186 / 100 leaf resolutions and took +2.045 / 1.857 / 1.870 / 1.879 seconds. Reference checks compare frontier +hashes, selected loss terms, score, and both normalization floors. Tokens, +movement counts, terminal-label counts, and maximum labels per state match +the prior replay on every row. The old replay did not record frontier hashes +or scores, so full decision equality is claimed for the sampled reference +checks, not for all 500 historical rows. + +The previous timing column is the archived pre-rebase `5c55a26` replay, not +a fresh 500-sample run of the #110 parent. The paired large-fixture table +above uses the current object backend. Replay high-water observed RSS was +3.285 GB, including the additional object-reference solves in the same +process; it is not a packed-only memory comparison. + +Local receipts: `/Users/antra/sill-cm/data/solver-fixtures/packed-selective-20260921.t0hKsG/`. +The `replay-500/` directory contains `config.json`, `rows.jsonl`, and +`summary.json`. CI also passed on macOS and Ubuntu with Node 20 and 24. + The implementation is on a separate follow-up branch; no live runtime or resident configuration was changed.