Gemma4-WebGPU / src /hooks /LLMProvider.tsx
BryanBradfo's picture
Gemma 4 multimodal WebGPU detection Space
576d07a
Raw
History Blame Contribute Delete
6.69 kB
import { useRef, useState, useCallback, type ReactNode } from "react";
import {
AutoProcessor,
Gemma4ForConditionalGeneration,
TextStreamer,
load_image,
} from "@huggingface/transformers";
import { LLMContext, type LoadingStatus, type DetectionResult } from "./LLMContext";
import { parseJsonDetection } from "../utils/detection-parser";
const MODEL_ID = "onnx-community/gemma-4-E2B-it-ONNX";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyModel = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyProcessor = any;
async function pickDtype(): Promise<"q4f16" | "q4"> {
try {
const adapter = await navigator.gpu?.requestAdapter({
powerPreference: "high-performance",
});
if (adapter) {
const features = [...adapter.features];
console.log("[Gemma4] WebGPU adapter features:", features);
console.log("[Gemma4] WebGPU adapter limits:", adapter.limits);
if (adapter.features.has("shader-f16")) {
console.log("[Gemma4] shader-f16 is AVAILABLE — using q4f16 (fast)");
return "q4f16";
}
console.log("[Gemma4] shader-f16 NOT available — falling back to q4 (slow)");
}
} catch (err) {
console.warn("[Gemma4] Adapter probe failed:", err);
}
return "q4";
}
export function LLMProvider({ children }: { children: ReactNode }) {
const modelRef = useRef<Promise<AnyModel> | null>(null);
const processorRef = useRef<AnyProcessor | null>(null);
const [status, setStatus] = useState<LoadingStatus>({ state: "idle" });
const [isGenerating, setIsGenerating] = useState(false);
const isGeneratingRef = useRef(false);
const [tps, setTps] = useState(0);
const [dtype, setDtype] = useState<"q4f16" | "q4" | null>(null);
const [result, setResult] = useState<DetectionResult | null>(null);
const abortRef = useRef(false);
isGeneratingRef.current = isGenerating;
const loadModel = useCallback(() => {
if (modelRef.current) return;
modelRef.current = (async () => {
setStatus({ state: "loading", message: "Loading processor..." });
try {
const pickedDtype = await pickDtype();
setDtype(pickedDtype);
console.log(`[Gemma4] Using dtype: ${pickedDtype}`);
const processor = await AutoProcessor.from_pretrained(MODEL_ID);
processorRef.current = processor;
setStatus({ state: "loading", message: "Downloading model..." });
const model = await Gemma4ForConditionalGeneration.from_pretrained(MODEL_ID, {
dtype: pickedDtype,
device: "webgpu",
progress_callback: (p: { status: string; progress?: number }) => {
// Use aggregated progress_total event for smooth, monotonic progress.
if (p.status === "progress_total" && typeof p.progress === "number") {
setStatus({
state: "loading",
progress: p.progress,
message: "Loading model...",
});
}
},
});
setStatus({ state: "ready" });
return model;
} catch (err) {
console.error("[Gemma4] Model loading failed:", err);
const msg = err instanceof Error ? err.message : String(err);
setStatus({ state: "error", error: msg });
modelRef.current = null;
throw err;
}
})();
}, []);
const detect = useCallback(async (imageUrl: string, prompt: string) => {
if (!modelRef.current) {
console.warn("[Gemma4] detect() called before model loaded");
return;
}
if (isGeneratingRef.current) {
console.warn("[Gemma4] detect() called while already generating");
return;
}
console.log("[Gemma4] Starting detection...");
setIsGenerating(true);
setTps(0);
setResult(null);
abortRef.current = false;
try {
const model = await modelRef.current;
const processor = processorRef.current!;
// Build multimodal message
const messages = [
{
role: "user",
content: [
{ type: "image" as const },
{ type: "text" as const, text: prompt },
],
},
];
// Apply chat template
const text = processor.apply_chat_template(messages, {
enable_thinking: false,
add_generation_prompt: true,
});
// Load and process image
const image = await load_image(imageUrl);
const inputs = await processor(text, image, null, {
add_special_tokens: false,
});
// Stream generation
let outputText = "";
let tokenCount = 0;
let firstTokenTime = 0;
const streamer = new TextStreamer(processor.tokenizer, {
skip_prompt: true,
skip_special_tokens: true,
callback_function: (token: string) => {
if (abortRef.current) return;
outputText += token;
const detections = parseJsonDetection(outputText);
setResult({ text: outputText, detections });
},
token_callback_function: () => {
tokenCount++;
if (tokenCount === 1) {
firstTokenTime = performance.now();
} else {
const elapsed = (performance.now() - firstTokenTime) / 1000;
if (elapsed > 0) {
setTps(Math.round(((tokenCount - 1) / elapsed) * 10) / 10);
}
}
},
});
await model.generate({
...inputs,
max_new_tokens: 128,
do_sample: true,
temperature: 1.0,
top_p: 0.95,
top_k: 64,
streamer,
});
// Final parse
const detections = parseJsonDetection(outputText);
setResult({ text: outputText, detections });
console.log(`[Gemma4] Detection done. ${detections.length} objects found.`);
} catch (err) {
console.error("[Gemma4] Detection error:", err);
const rawMsg = err instanceof Error ? err.message : String(err);
const isOOM =
rawMsg.includes("OUT_OF_DEVICE_MEMORY") ||
rawMsg.includes("Invalid Buffer") ||
rawMsg.includes("Softmax");
const friendly = isOOM
? "GPU ran out of memory. Enable chrome://flags/#enable-unsafe-webgpu and chrome://flags/#enable-vulkan, restart Chrome, and close other GPU-heavy tabs."
: rawMsg;
setResult({ text: `Error: ${friendly}`, detections: [] });
}
setIsGenerating(false);
}, []);
const stop = useCallback(() => {
abortRef.current = true;
}, []);
return (
<LLMContext.Provider
value={{ status, isGenerating, tps, dtype, result, loadModel, detect, stop }}
>
{children}
</LLMContext.Provider>
);
}