Fixed for decoder dl speed and mobile warnings on low memory devices.
This commit is contained in:
+48
-18
@@ -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)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user