Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/ml/imf/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { verifyAndRead, parseManifest, type IMFManifest } from "./loader.js"
import { resolve } from "./registry.js"
import { normalizeArabicInput, repetitionGuardCut } from "./guards.js"
import { EOS_ID, PAD_ID, decode, encode } from "./tokens.js"
import { translateWindowed } from "./windows.js"

interface InputMeta {
readonly name: string
Expand Down Expand Up @@ -90,6 +91,12 @@ export class IMFModel {
}

async translate(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise<string> {
// long inputs are out-of-distribution: split at the training
// budget (single-window inputs pass straight through)
return translateWindowed(this, text, maxLen, opts)
}

async translateDirect(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise<string> {
const hidden = await this.encode(text, opts)
if (!hidden) return ""
const tokens = this.kv
Expand Down
5 changes: 5 additions & 0 deletions src/ml/imf/speculative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import { normalizeArabicInput, repetitionGuardCut } from "./guards.js"
import { EOS_ID, decode, encode } from "./tokens.js"
import type { IMFModel, DecodeOptions, DecodeCursor } from "./model.js"
import { translateWindowed } from "./windows.js"

export interface SpeculativeOptions {
/** draft tokens per verifier pass (default 8) */
Expand Down Expand Up @@ -105,6 +106,10 @@ export class SpeculativeModel {
}

async translate(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise<string> {
return translateWindowed(this, text, maxLen, opts)
}

async translateDirect(text: string, maxLen = 256, opts: DecodeOptions = {}): Promise<string> {
const normalized = opts.raw === true ? text : normalizeArabicInput(text)
this.lastNormalized = normalized
if (encode(normalized).length === 1) return ""
Expand Down
51 changes: 51 additions & 0 deletions src/ml/imf/windows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Windowed long-input handling (the runtime side of the 1400-byte
* protocol): models train on <=1400-byte windows, so longer inputs are
* out-of-distribution and degrade. splitWindows reproduces the
* published harness split exactly (word-boundary, byte-budget);
* translateWithWindows decodes each window and joins with spaces.
*/

export const BYTE_BUDGET = 1400

/** Split at word boundaries so no window exceeds the byte budget.
* Identical to the Python harness's split_windows. */
export function splitWindows(text: string, budget: number = BYTE_BUDGET): string[] {
if (new TextEncoder().encode(text).length <= budget) return [text]
const windows: string[] = []
let current: string[] = []
let n = 0
// Python str.split(): runs of whitespace, no empty tokens
for (const word of text.split(/\s+/).filter((w) => w !== "")) {
const cost = new TextEncoder().encode(word).length + 1
if (current.length > 0 && n + cost > budget) {
windows.push(current.join(" "))
current = []
n = 0
}
current.push(word)
n += cost
}
if (current.length > 0) windows.push(current.join(" "))
return windows
}

import type { DecodeOptions } from "./model.js"

/** Decode text through `model`, splitting at the training budget when
* the input is long. Single-window inputs (all golden-set sizes) pass
* through untouched — cross-runtime parity is unaffected. Both
* IMFModel.translate and SpeculativeModel.translate route through
* this, so long inputs never reach a model out-of-distribution. */
export async function translateWindowed(
model: { translateDirect(text: string, maxLen?: number, opts?: DecodeOptions): Promise<string> },
text: string,
maxLen: number,
opts: DecodeOptions = {},
): Promise<string> {
const windows = splitWindows(text)
if (windows.length <= 1) return model.translateDirect(text, maxLen, opts)
const parts: string[] = []
for (const w of windows) parts.push(await model.translateDirect(w, maxLen, opts))
return parts.join(" ")
}
Loading
Loading