Fixed for decoder dl speed and mobile warnings on low memory devices.

This commit is contained in:
Lucas Winther
2026-07-29 07:47:42 +02:00
parent 2643a446d0
commit 78e99fc415
7 changed files with 208 additions and 32 deletions
+48 -18
View File
@@ -213,6 +213,38 @@ export function App() {
return deviceOverrides.current.get(overrideKey(forModelId, preference)) ?? preference;
}
/** Mirrors ModelPicker's size-estimate fallback: until the worker reports an
* active device, assume WASM unless the user asked for an accelerator. */
function guessDevice(preference: DevicePreference): InferenceDevice {
return (
activeDevice ??
(preference === "webgpu" ? "webgpu" : preference === "webnn" ? "webnn" : "wasm")
);
}
/**
* Whether loading `forModelId` on `device` risks crashing a mobile tab
* (D-027) — independent of whether the user has already been asked. Used
* both to decide whether to show the confirm dialog and, regardless of the
* user's answer, to decide whether the worker should load sub-model
* sessions sequentially (memory-safe but slower) or in parallel (fast,
* safe everywhere this isn't true).
*/
function isMobileLoadRisk(forModelId: string, device: InferenceDevice): boolean {
const targetModel = getModel(forModelId);
if (!targetModel) return false;
const peakBytes = estimatePeakLoadBytes(targetModel, resolveDefaultDtype(forModelId, device));
const hint: MobileMemoryHint = {
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
maxTouchPoints: typeof navigator !== "undefined" ? navigator.maxTouchPoints : undefined,
deviceMemoryGB:
typeof navigator !== "undefined"
? (navigator as Navigator & { deviceMemory?: number }).deviceMemory
: undefined,
};
return shouldWarnBeforeLoad(hint, peakBytes);
}
/**
* Confirm before a browser-engine load that risks crashing the tab on
* mobile (D-027). Returns true if the load should proceed — either it
@@ -223,25 +255,12 @@ export function App() {
function confirmRiskyMobileLoad(forModelId: string, preference: DevicePreference): boolean {
const targetModel = getModel(forModelId);
if (!targetModel) return true;
// Mirrors ModelPicker's size-estimate fallback: until the worker reports
// an active device, assume WASM unless the user asked for an accelerator.
const device: InferenceDevice =
activeDevice ??
(preference === "webgpu" ? "webgpu" : preference === "webnn" ? "webnn" : "wasm");
const device = guessDevice(preference);
const key = overrideKey(forModelId, device);
if (mobileWarningAcknowledged.current.has(key)) return true;
if (!isMobileLoadRisk(forModelId, device)) return true;
const peakBytes = estimatePeakLoadBytes(targetModel, resolveDefaultDtype(forModelId, device));
const hint: MobileMemoryHint = {
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
maxTouchPoints: typeof navigator !== "undefined" ? navigator.maxTouchPoints : undefined,
deviceMemoryGB:
typeof navigator !== "undefined"
? (navigator as Navigator & { deviceMemory?: number }).deviceMemory
: undefined,
};
if (!shouldWarnBeforeLoad(hint, peakBytes)) return true;
const proceed = window.confirm(
`${targetModel.displayName} needs roughly ${formatModelSize(peakBytes)} of memory at ` +
"once to load — more than many phones and tablets allow a browser tab to use " +
@@ -379,6 +398,7 @@ export function App() {
type: "load",
modelId: message.modelId,
device: message.device,
sequentialSessionLoad: isMobileLoadRisk(message.modelId, message.device),
});
break;
}
@@ -417,10 +437,16 @@ export function App() {
setLoadStarted(true);
setError(null);
newLoadGate();
const device = effectiveDevice(nextModelId, preference);
postToWorker(worker, {
type: "load",
modelId: nextModelId,
device: effectiveDevice(nextModelId, preference),
device,
// D-027/D-029: sub-model sessions load sequentially (memory-safe, but
// slower — halves the CDN concurrency a fast connection could otherwise
// use) only where the peak-memory crash risk is real; everywhere else
// (desktop, low-risk models) the worker loads them in parallel.
sequentialSessionLoad: isMobileLoadRisk(nextModelId, guessDevice(device)),
});
return worker;
}
@@ -540,10 +566,12 @@ export function App() {
if (!loadStarted) {
startLoad(modelId, devicePreference);
} else {
const device = effectiveDevice(modelId, devicePreference);
postToWorker(workerRef.current!, {
type: "load",
modelId,
device: effectiveDevice(modelId, devicePreference),
device,
sequentialSessionLoad: isMobileLoadRisk(modelId, guessDevice(device)),
});
}
}
@@ -788,10 +816,12 @@ export function App() {
if (!loadStarted) {
startLoad(modelId, devicePreference);
} else {
const device = effectiveDevice(modelId, devicePreference);
postToWorker(worker, {
type: "load",
modelId,
device: effectiveDevice(modelId, devicePreference),
device,
sequentialSessionLoad: isMobileLoadRisk(modelId, guessDevice(device)),
});
}
+11 -4
View File
@@ -52,11 +52,18 @@ export function isLikelyMobile(
/**
* Fraction of reported device RAM a single tab can safely peak at before
* mobile OSes intervene. Conservative on purpose — `deviceMemory` already
* caps at 8 regardless of actual RAM, and the tab competes with the rest of
* the OS, the browser process itself, and other open tabs.
* mobile OSes intervene.
*
* D-029: the original 0.5 was not conservative enough — `deviceMemory` is
* commonly 2-4 on ordinary (not just flagship) Android phones today, and at
* 0.5 that already yields a 1-2 GB "safe" budget, clearing every model in
* the catalog including Medium (~556 MB peak) and Large v3 Turbo (~762 MB) —
* silently skipping the warning for exactly the phones D-027's crash report
* was about. 0.1 keeps mid/low-end phones (deviceMemory ≤ 4) warned for
* Medium+ while still exempting genuine high-end devices (deviceMemory ≥ 6,
* 600+ MB budget) that have real headroom to spare.
*/
const SAFE_TAB_FRACTION_OF_DEVICE_MEMORY = 0.5;
const SAFE_TAB_FRACTION_OF_DEVICE_MEMORY = 0.1;
/**
* Fallback risk threshold when `deviceMemory` isn't reported (notably all of
+8 -2
View File
@@ -13,6 +13,7 @@ import {
pipeline,
WhisperTextStreamer,
type AutomaticSpeechRecognitionPipeline,
type PretrainedModelOptions,
type ProgressInfo,
} from "@huggingface/transformers";
import { resolveDefaultDtype } from "../lib/models";
@@ -256,7 +257,7 @@ async function createPipeline(
configureWasmThreads(device);
const dtype = message.dtype ?? resolveDefaultDtype(message.modelId, device);
try {
const asr = await pipeline("automatic-speech-recognition", message.modelId, {
const pipelineOptions: PretrainedModelOptions & { sequentialSessionLoad?: boolean } = {
device,
dtype,
// WASM sessions cap graph optimization at "basic" (D-014): the level-2
@@ -268,7 +269,12 @@ async function createPipeline(
session_options: { graphOptimizationLevel: "basic" as const },
}),
progress_callback: forwardProgress,
});
// D-027/D-029: sequential sub-model session loading trades load speed
// for a lower peak-memory footprint — only worth it where the UI
// determined this load risks crashing a mobile tab.
sequentialSessionLoad: message.sequentialSessionLoad ?? false,
};
const asr = await pipeline("automatic-speech-recognition", message.modelId, pipelineOptions);
// What ort actually ended up with (it clamps to 1 without isolation) —
// surfaced to the UI so degraded threading is visible in the field.
const threads = env.backends.onnx.wasm?.numThreads ?? 1;
+10
View File
@@ -47,6 +47,16 @@ export interface LoadMessage {
* default for the device it actually selected (src/lib/models.ts).
*/
dtype?: DtypeSpec;
/**
* Load the encoder/decoder ONNX sessions one at a time instead of
* concurrently (D-027/D-029). Sequential halves the peak JS-heap/WASM
* memory a load needs (avoiding mobile tab crashes on larger models) at
* the cost of losing the CDN-concurrency benefit a fast connection would
* otherwise get from two simultaneous downloads — worth paying only where
* the memory-crash risk is real. The UI computes this (`isMobileLoadRisk`
* in App.tsx); omitted/false loads in parallel.
*/
sequentialSessionLoad?: boolean;
}
export interface TranscribeMessage {