/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * ONNX inference wrapper. * * Loads a token-classification model exported by `packages/corpus-python/src/mailwoman_train/ * export_onnx.py` (BertForTokenClassification w/ inputs `input_ids` + `attention_mask`, output * `logits` shape `[batch, sequence, num_labels]`). * * Lazy-loads on first `infer()` call unless `warmup: true` is passed; the constructor itself is * cheap and synchronous. */ export { LOCALITY_SURFACE_FEATURE_DIM, STREET_TYPE_FEATURE_DIM } from "./gazetteer-inference.ts"; /** * Evidence-bundle zero-fallback widths (Option-A; must match the trained model's channel dims). */ /** * Channel width of the locality-surface evidence feature. Must match the trained model's input shape. */ export interface ONNXRunnerOpts { /** * If true, load the model immediately in `create()`. Default false. */ warmup?: boolean; /** * Fixed sequence length the model expects. v0.1.0 / v0.2.0 quantization baked in 128 (the training-time max position) * even though the fp32 export specified dynamic axes — re-quantize with a different shape to override. Inputs shorter * than this are padded with id `0` and masked out via attention_mask=0; inputs longer are truncated. */ fixedSeqLen?: number; /** * ONNX Runtime execution providers to try, in priority order — e.g. `["cuda", "cpu"]` or `["webgpu", "cpu"]`. * **Default `["cpu"]`** (unchanged behavior). GPU providers (`cuda`, `webgpu`) THROW at session-create when their * runtime/driver is absent rather than soft-falling-back, so this is **guarded**: if the requested list fails to * initialize, the runner retries on CPU alone. The cost of a failed GPU probe is a one-time sub-`100 ms` hit at load, * so a GPU box lights up and a CPU box pays ~nothing. `cpu` is always appended if not present. */ executionProviders?: string[]; /** * Cap ONNX Runtime's INTRA-op thread pool — the threads a single operator splits its work across. * * Unset means ORT sizes the pool to the machine's core count. That is the right default for a server running one * session over long sequences, and the wrong one for the shape this repo actually runs: short addresses, frequently * several processes at once. Every CLI invocation is its own session, so N concurrent processes each claim every * core, and the oversubscription surfaces as latency rather than error — measured 2026-08-03, eight concurrent * `mailwoman geocode` calls took 8.75 s against a 10 s test timeout on an otherwise idle 16-core box, where one alone * took 5.62 s. * * Set it when the caller knows it is one of many, or when sequences are short enough that thread coordination costs * more than the parallelism returns. */ intraOpNumThreads?: number; } /** * Default sequence length for v0.1.0 / v0.2.0 (BertConfig max_position_embeddings = 128). */ export declare const DEFAULT_FIXED_SEQ_LEN = 128; /** * Intra-op thread cap applied by `NeuralAddressClassifier.loadFromWeights`, overridable per-process via * `MAILWOMAN_INTRA_OP_THREADS`. * * THERE IS NO VALUE THAT IS RIGHT FOR BOTH REGIMES, which is why this is a knob with a compromise default rather than a * tuned constant. Measured on a 16-core box: * * - ONE process, 120 warm parses: 1 thread 18.3 ms/parse, 2 threads 12.5, 4 threads 9.2, ORT's all-cores default 9.3. * More threads win; the parallelism is doing real work. * - FOUR concurrent processes, full geocode: 1 thread 32 req/s each, 2 threads 45, 4 threads 33. Fewer threads win, * because N processes each sizing a pool to the machine oversubscribe it N-fold. * * Two is the compromise: it costs a single process ~35% latency against its own optimum, and buys a four-process server * ~36% throughput against the single-process optimum applied blindly. A server that knows its own worker count should * set `MAILWOMAN_INTRA_OP_THREADS` to roughly cores/workers instead of accepting this. * * Re-derive both curves before changing it. They are properties of the model and the box, and the single-process one * alone will point at the wrong answer. */ export declare const DEFAULT_INTRA_OP_THREADS = 2; export interface InferResult { /** * Logits per token per label, indexed as `logits[tokenIdx][labelIdx]`. */ logits: number[][]; /** * Number of label classes (the inner-dim of the logits tensor). */ numLabels: number; /** * Pooled locale-head posterior (`locale_logits` output, LOCALE_COUNTRIES order), when the model exports it (v1.1.0+, * #511 Tier A). Absent on older bundles — consumers must treat undefined as "no address-system detection available". */ localeLogits?: number[]; /** * #727 stage-2: per-span type scores from the semi-Markov span head (`span_scores` output, v3.x+). Indexed * `spanScores[tokenIdx][lengthIdx][segmentTypeIdx]` — the segment starting at `tokenIdx`, of length `lengthIdx + 1` * tokens, typed `SEGMENT_TYPES[segmentTypeIdx]` (that axis ships in the weights bundle's `semi-crf-transitions.json`, * never hardcoded — the PLACETYPE_ORDER class). * * Absent on every pre-v3 bundle, so consumers MUST treat undefined as "no span decode available" and fall back to the * BIO path. Fetching it costs ~0.75 ms (CPU, S=128); a runtime that never reads it pays nothing (ORT prunes the * unfetched branch) — measured in `docs/articles/evals/2026-07-15-v301-phase2-export.md`. */ spanScores?: number[][][]; /** * Max span length (the `L` axis of {@link spanScores}). Absent iff `spanScores` is. */ maxSpan?: number; } export declare class ONNXRunner { private session; private loadPromise; readonly fixedSeqLen: number; private readonly executionProviders; private readonly intraOpNumThreads; private readonly modelPath; private readonly modelBytes; private constructor(); /** * Load by path. Reads the model lazily unless `warmup` is true. */ static create(modelPath: string, opts?: ONNXRunnerOpts): Promise; /** * Load from an already-read byte buffer. */ static fromBytes(modelBytes: Uint8Array, opts?: ONNXRunnerOpts): Promise; private ensureSession; /** * Create the session on the configured execution providers, guarded: GPU providers (`cuda`/`webgpu`) throw at * create-time when their runtime/driver is missing, so on failure we retry on CPU alone. A box with the GPU runtime * uses it; a box without one transparently lands on CPU. */ private createSession; /** * Run inference on a single token id sequence. * * Pads to `fixedSeqLen` (default 128) with id 0 + mask 0; truncates if longer. Output is sliced back to the actual * input length. * * @param tokenIDs The id sequence produced by the tokenizer (no special tokens added). * @param anchor Optional postcode-anchor channel (#239/#240). When supplied (only for anchor models — exported with * the `anchor_features`/`anchor_confidence` inputs), per-piece features `(seqLen × dim)` + confidence `(seqLen,)` * are fed, zero-padded to `fixedSeqLen`. Omit for plain models, whose ONNX has no anchor inputs. */ infer(tokenIDs: number[], anchor?: { features: ReadonlyArray>; confidence: ReadonlyArray; }, gazetteer?: { features: ReadonlyArray>; confidence: ReadonlyArray; }, country?: { features: ReadonlyArray>; confidence: ReadonlyArray; }, evidence?: { streetType?: { features: ReadonlyArray>; confidence: ReadonlyArray; }; localitySurface?: { features: ReadonlyArray>; confidence: ReadonlyArray; }; }): Promise; /** * The model's declared input names (loads the session if not already loaded). Used by the ProductionScorer (#718) * back-compat path: when a model-card has no `requires` block, the required soft-feature channels are INFERRED from * the graph — a model exporting `anchor_features` / `gazetteer_features` declared those channels mandatory at train * time. */ inputNames(): Promise; } //# sourceMappingURL=onnx-runner.d.ts.map