polats commited on
Commit
b9bfeb6
·
1 Parent(s): 1b55391

Move Nemotron text generation to WebGPU

Browse files
Files changed (3) hide show
  1. app.py +0 -12
  2. web/engineServer.js +0 -1
  3. web/engineTransformers.js +18 -4
app.py CHANGED
@@ -265,7 +265,6 @@ TTS_MODE = os.environ.get("TINY_TTS_MODE", "").strip().lower()
265
  VOXCPM_SPACE = os.environ.get("TINY_VOXCPM_SPACE", "").strip()
266
  TINY_AYA_SPACE = os.environ.get("TINY_AYA_SPACE", "").strip()
267
  MINICPM5_SPACE = os.environ.get("TINY_MINICPM5_SPACE", "").strip()
268
- NEMOTRON_SPACE = os.environ.get("TINY_NEMOTRON_SPACE", "").strip()
269
  _local_tts = None # VoiceDesign model
270
  _local_clone = None # Base model (voice clone) — lazy, only if a clone is requested
271
  _local_tts_lock = threading.Lock()
@@ -514,10 +513,6 @@ def _minicpm5_stream(system, user, max_tokens, temperature):
514
  yield from _space_text_stream(MINICPM5_SPACE, system, user, max_tokens, temperature)
515
 
516
 
517
- def _nemotron_stream(system, user, max_tokens, temperature):
518
- yield from _space_text_stream(NEMOTRON_SPACE, system, user, max_tokens, temperature)
519
-
520
-
521
  @fastapi_app.post("/voxcpm-tts")
522
  async def voxcpm_tts(request: Request):
523
  body = await request.json()
@@ -824,13 +819,6 @@ async def text_generate_stream(request: Request):
824
  if stop.is_set():
825
  break
826
  loop.call_soon_threadsafe(q.put_nowait, ("delta", chunk))
827
- elif model == "nemotron-3-nano-4b-zerogpu":
828
- if not NEMOTRON_SPACE:
829
- raise llm.LlmUnavailable("TINY_NEMOTRON_SPACE not set")
830
- for chunk in _nemotron_stream(system, user, max_tokens, temperature):
831
- if stop.is_set():
832
- break
833
- loop.call_soon_threadsafe(q.put_nowait, ("delta", chunk))
834
  else:
835
  for chunk in llm.stream_chat(
836
  system,
 
265
  VOXCPM_SPACE = os.environ.get("TINY_VOXCPM_SPACE", "").strip()
266
  TINY_AYA_SPACE = os.environ.get("TINY_AYA_SPACE", "").strip()
267
  MINICPM5_SPACE = os.environ.get("TINY_MINICPM5_SPACE", "").strip()
 
268
  _local_tts = None # VoiceDesign model
269
  _local_clone = None # Base model (voice clone) — lazy, only if a clone is requested
270
  _local_tts_lock = threading.Lock()
 
513
  yield from _space_text_stream(MINICPM5_SPACE, system, user, max_tokens, temperature)
514
 
515
 
 
 
 
 
516
  @fastapi_app.post("/voxcpm-tts")
517
  async def voxcpm_tts(request: Request):
518
  body = await request.json()
 
819
  if stop.is_set():
820
  break
821
  loop.call_soon_threadsafe(q.put_nowait, ("delta", chunk))
 
 
 
 
 
 
 
822
  else:
823
  for chunk in llm.stream_chat(
824
  system,
web/engineServer.js CHANGED
@@ -7,7 +7,6 @@ const MODELS = [
7
  { id: 'server-local', label: 'Configured server model', params: 'local/remote', note: 'uses TINY_LLM_* on the Space or local app' },
8
  { id: 'tiny-aya-global-zerogpu', label: 'Tiny Aya Global 3.35B', params: '3.35B', note: 'ZeroGPU sidecar; multilingual' },
9
  { id: 'minicpm5-1b-zerogpu', label: 'MiniCPM5 1B', params: '1B', note: 'ZeroGPU sidecar; efficient MiniCPM5 text model' },
10
- { id: 'nemotron-3-nano-4b-zerogpu', label: 'Nemotron 3 Nano 4B', params: '4B', note: 'ZeroGPU sidecar; GGUF llama.cpp, slower startup' },
11
  ]
12
  const get = (id) => MODELS.find((m) => m.id === id) || MODELS[0]
13
 
 
7
  { id: 'server-local', label: 'Configured server model', params: 'local/remote', note: 'uses TINY_LLM_* on the Space or local app' },
8
  { id: 'tiny-aya-global-zerogpu', label: 'Tiny Aya Global 3.35B', params: '3.35B', note: 'ZeroGPU sidecar; multilingual' },
9
  { id: 'minicpm5-1b-zerogpu', label: 'MiniCPM5 1B', params: '1B', note: 'ZeroGPU sidecar; efficient MiniCPM5 text model' },
 
10
  ]
11
  const get = (id) => MODELS.find((m) => m.id === id) || MODELS[0]
12
 
web/engineTransformers.js CHANGED
@@ -6,6 +6,7 @@ const MODELS = [
6
  { id: 'qwen2.5-0.5b', label: 'Qwen2.5 0.5B', params: '0.5B', repo: 'onnx-community/Qwen2.5-0.5B-Instruct' },
7
  { id: 'smollm2-360m', label: 'SmolLM2 360M', params: '360M', repo: 'HuggingFaceTB/SmolLM2-360M-Instruct' },
8
  { id: 'llama3.2-1b', label: 'Llama 3.2 1B', params: '1B', repo: 'onnx-community/Llama-3.2-1B-Instruct' },
 
9
  ]
10
  const get = (id) => MODELS.find((m) => m.id === id) || MODELS[0]
11
 
@@ -26,7 +27,7 @@ async function pickDevice() {
26
  }
27
 
28
  let _lib = null, _pipe = null, _loadedId = null, _loadingId = null, _loadPromise = null, _device = 'wasm', _chain = Promise.resolve()
29
- async function lib() { if (!_lib) _lib = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3'); return _lib }
30
 
31
  async function ensure(id, onProgress) {
32
  const m = get(id)
@@ -39,13 +40,14 @@ async function ensure(id, onProgress) {
39
  _loadPromise = (async () => {
40
  const { pipeline } = await lib()
41
  _device = await pickDevice()
 
42
  const mk = (device) => pipeline('text-generation', m.repo, {
43
  device, dtype: 'q4',
44
  progress_callback: (p) => { if (onProgress && p.status === 'progress' && p.total) onProgress(p.loaded / p.total) },
45
  })
46
  let pipe
47
  try { pipe = await mk(_device) }
48
- catch (e) { if (_device !== 'wasm') { _device = 'wasm'; pipe = await mk('wasm') } else throw e }
49
  _pipe = pipe; _loadedId = m.id; return pipe
50
  })().catch((e) => { _loadPromise = null; _loadingId = null; throw e })
51
  return _loadPromise
@@ -62,8 +64,20 @@ function stream(id, system, user, { maxTokens = 200, temperature = 0.8, onToken,
62
  callback_function: (text) => { if (!text) return; full += text; if (onToken) onToken(text); st.tick() },
63
  })
64
  const messages = [{ role: 'system', content: system }, { role: 'user', content: user }]
65
- const prompt = pipe.tokenizer.apply_chat_template(messages, { tokenize: false, add_generation_prompt: true })
66
- await pipe(prompt, { max_new_tokens: maxTokens, do_sample: true, temperature, top_k: 40, top_p: 0.9, streamer })
 
 
 
 
 
 
 
 
 
 
 
 
67
  return { text: full, stats: st.finish() }
68
  }
69
  const p = _chain.then(run, run); _chain = p.catch(() => {}); return p
 
6
  { id: 'qwen2.5-0.5b', label: 'Qwen2.5 0.5B', params: '0.5B', repo: 'onnx-community/Qwen2.5-0.5B-Instruct' },
7
  { id: 'smollm2-360m', label: 'SmolLM2 360M', params: '360M', repo: 'HuggingFaceTB/SmolLM2-360M-Instruct' },
8
  { id: 'llama3.2-1b', label: 'Llama 3.2 1B', params: '1B', repo: 'onnx-community/Llama-3.2-1B-Instruct' },
9
+ { id: 'nemotron-3-nano-4b', label: 'Nemotron 3 Nano 4B', params: '4B', repo: 'onnx-community/NVIDIA-Nemotron-3-Nano-4B-BF16-ONNX', webgpuOnly: true, note: 'WebGPU only; large browser download' },
10
  ]
11
  const get = (id) => MODELS.find((m) => m.id === id) || MODELS[0]
12
 
 
27
  }
28
 
29
  let _lib = null, _pipe = null, _loadedId = null, _loadingId = null, _loadPromise = null, _device = 'wasm', _chain = Promise.resolve()
30
+ async function lib() { if (!_lib) _lib = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.0.0-next.8'); return _lib }
31
 
32
  async function ensure(id, onProgress) {
33
  const m = get(id)
 
40
  _loadPromise = (async () => {
41
  const { pipeline } = await lib()
42
  _device = await pickDevice()
43
+ if (m.webgpuOnly && _device !== 'webgpu') throw new Error(`${m.label} requires WebGPU support in this browser.`)
44
  const mk = (device) => pipeline('text-generation', m.repo, {
45
  device, dtype: 'q4',
46
  progress_callback: (p) => { if (onProgress && p.status === 'progress' && p.total) onProgress(p.loaded / p.total) },
47
  })
48
  let pipe
49
  try { pipe = await mk(_device) }
50
+ catch (e) { if (!m.webgpuOnly && _device !== 'wasm') { _device = 'wasm'; pipe = await mk('wasm') } else throw e }
51
  _pipe = pipe; _loadedId = m.id; return pipe
52
  })().catch((e) => { _loadPromise = null; _loadingId = null; throw e })
53
  return _loadPromise
 
64
  callback_function: (text) => { if (!text) return; full += text; if (onToken) onToken(text); st.tick() },
65
  })
66
  const messages = [{ role: 'system', content: system }, { role: 'user', content: user }]
67
+ if (get(id).id === 'nemotron-3-nano-4b') {
68
+ await pipe(messages, {
69
+ max_new_tokens: maxTokens,
70
+ do_sample: true,
71
+ temperature,
72
+ top_k: 40,
73
+ top_p: 0.9,
74
+ streamer,
75
+ tokenizer_encode_kwargs: { enable_thinking: false },
76
+ })
77
+ } else {
78
+ const prompt = pipe.tokenizer.apply_chat_template(messages, { tokenize: false, add_generation_prompt: true })
79
+ await pipe(prompt, { max_new_tokens: maxTokens, do_sample: true, temperature, top_k: 40, top_p: 0.9, streamer })
80
+ }
81
  return { text: full, stats: st.finish() }
82
  }
83
  const p = _chain.then(run, run); _chain = p.catch(() => {}); return p