airayven7 commited on
Commit
bfa15f4
Β·
verified Β·
1 Parent(s): ee410d7

Sync from GitHub 50c0a1f

Browse files
README.md CHANGED
@@ -13,7 +13,7 @@ app_file: app.py
13
  pinned: false
14
  preload_from_hub:
15
  - nvidia/nemotron-colembed-vl-4b-v2
16
- - openbmb/MiniCPM5-1B
17
  - openbmb/MiniCPM-V-4_5
18
  - nvidia/llama-nemotron-embed-vl-1b-v2
19
  license: mit
 
13
  pinned: false
14
  preload_from_hub:
15
  - nvidia/nemotron-colembed-vl-4b-v2
16
+ - openbmb/MiniCPM4.1-8B
17
  - openbmb/MiniCPM-V-4_5
18
  - nvidia/llama-nemotron-embed-vl-1b-v2
19
  license: mit
core/constants.py CHANGED
@@ -45,20 +45,45 @@ MINICPM_AGENT_REVISION = os.environ.get("MINICPM_AGENT_REVISION", "") or None
45
 
46
  # Selectable agent brains, offered in the UI settings panel. ONE model is meant
47
  # to be resident in VRAM at a time β€” switching evicts the previous and loads the
48
- # next (models/minicpm_agent.use_model). NOTE: evicting the FIRST/default brain
49
- # frees nothing β€” it's loaded at import (ZeroGPU emulation phase) and materialized
50
- # into the forked GPU worker, where empty_cache() can't reclaim it; only a brain
51
- # that was itself switched in at runtime frees on eviction. So a switched-in brain
52
- # must fit in the headroom ABOVE the resident set + stuck default (~15 GiB free on
53
- # the 48 GiB `large` slice β€” see core/vram.py). That's why the 8B brains were
54
- # dropped (they need ~16 GiB) and the largest option here is 4B.
 
 
 
 
 
 
 
 
55
  # Each loads as an AutoModelForCausalLM; `trust_remote_code` (default False) flags
56
- # the ones that ship custom modeling code (MiniCPM3-4B). `thinking` flags whether
57
- # the chat template accepts enable_thinking (Qwen3 and MiniCPM5 do β€” tool routing
58
- # passes it False; MiniCPM3 does not). The FIRST entry is the default at boot and
59
- # tracks the MINICPM_AGENT_MODEL_ID/REVISION env overrides, so existing config
60
- # still applies. Only one is resident at a time, well under the params budget.
 
61
  AGENT_MODELS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  {
63
  "key": "minicpm5-1b",
64
  "label": "MiniCPM5 1B",
@@ -80,6 +105,17 @@ AGENT_MODELS = [
80
  "revision": None,
81
  "thinking": True,
82
  },
 
 
 
 
 
 
 
 
 
 
 
83
  {
84
  "key": "minicpm3-4b",
85
  "label": "MiniCPM3 4B",
@@ -96,6 +132,35 @@ AGENT_MODELS = [
96
  # switch time with ~5 GiB to spare after the grounding spike. The 8B (16
97
  # GiB) didn't fit β€” see core/vram.py / the find-turn VRAM logs.
98
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  ]
100
  DEFAULT_AGENT_MODEL = AGENT_MODELS[0]["key"]
101
  # A tool-call decision is short JSON; a rerank reply is a single number. 96 was
 
45
 
46
  # Selectable agent brains, offered in the UI settings panel. ONE model is meant
47
  # to be resident in VRAM at a time β€” switching evicts the previous and loads the
48
+ # next (models/minicpm_agent.use_model). NOTE: the FIRST/default brain is loaded at
49
+ # import (ZeroGPU emulation phase) and materialized into the forked GPU worker,
50
+ # where empty_cache() can't reclaim it β€” it stays stuck for the process. A brain
51
+ # SWITCHED IN at runtime must fit the headroom ABOVE the resident set
52
+ # (VLM+ColEmbed+embedder) + that stuck default (~15 GiB free on the 48 GiB `large`
53
+ # slice β€” see core/vram.py).
54
+ #
55
+ # The default is MiniCPM4.1-8B at int8 (~8.5 GiB via bitsandbytes). As the LONE
56
+ # stuck brain it REPLACES the old 1B default (it does not stack on it), so the
57
+ # resident set + one ~8.5 GiB brain still leaves room for the grounding spike β€” the
58
+ # same footprint class as MiniCPM3-4B, which was VRAM-vetted to fit. (bf16 8B at
59
+ # ~16 GiB still does NOT fit; int8 is what makes the 8B deployable as the default.)
60
+ # Caveat: with an ~8.5 GiB default already stuck, switching ANOTHER 4-8 GiB brain
61
+ # in at runtime for a UI A/B is tight and may OOM at grounding β€” the default path
62
+ # is fine.
63
  # Each loads as an AutoModelForCausalLM; `trust_remote_code` (default False) flags
64
+ # the ones that ship custom modeling code (MiniCPM3 / MiniCPM4.1). `thinking` flags
65
+ # whether the chat template accepts enable_thinking (Qwen3, MiniCPM5, MiniCPM4.1 do
66
+ # β€” tool routing passes it False; MiniCPM3 does not). int8/4bit brains need
67
+ # bitsandbytes (requirements.txt). The FIRST entry is the default at boot; the
68
+ # minicpm5-1b entry stays selectable and still tracks the MINICPM_AGENT_MODEL_ID/
69
+ # REVISION env overrides. Only one brain is resident at a time.
70
  AGENT_MODELS = [
71
+ {
72
+ "key": "minicpm4.1-8b-8bit",
73
+ "label": "MiniCPM4.1 8B (8-bit)",
74
+ "model_id": "openbmb/MiniCPM4.1-8B",
75
+ "revision": None,
76
+ # DEFAULT brain. Hybrid-reasoning 8B at int8 (~8.5 GiB) β€” the best
77
+ # deployable eval config (0.85 tool / 0.91 args; with the v3 prompt it also
78
+ # recovers half the coincidence fix). trust_remote_code custom modeling
79
+ # (sparse "InfLLM v2" attention); runs clean on current transformers,
80
+ # unlike MiniCPM3-4B. Quantized weights load straight onto the GPU, so
81
+ # device_map is set (skips the host→device .to copy).
82
+ "thinking": True,
83
+ "trust_remote_code": True,
84
+ "quantization": "8bit",
85
+ "device_map": {"": 0},
86
+ },
87
  {
88
  "key": "minicpm5-1b",
89
  "label": "MiniCPM5 1B",
 
105
  "revision": None,
106
  "thinking": True,
107
  },
108
+ {
109
+ "key": "qwen3-4b",
110
+ "label": "Qwen3 4B",
111
+ "model_id": "Qwen/Qwen3-4B",
112
+ "revision": None,
113
+ # Native Qwen3 arch (no trust_remote_code β†’ immune to the custom-modeling
114
+ # rot that breaks MiniCPM3-4B on current transformers). ~4B / ~8.1 GiB bf16,
115
+ # same footprint class as MiniCPM3-4B, which was VRAM-vetted to fit; routing
116
+ # passes enable_thinking=False.
117
+ "thinking": True,
118
+ },
119
  {
120
  "key": "minicpm3-4b",
121
  "label": "MiniCPM3 4B",
 
132
  # switch time with ~5 GiB to spare after the grounding spike. The 8B (16
133
  # GiB) didn't fit β€” see core/vram.py / the find-turn VRAM logs.
134
  },
135
+ {
136
+ "key": "minicpm4.1-8b",
137
+ "label": "MiniCPM4.1 8B",
138
+ "model_id": "openbmb/MiniCPM4.1-8B",
139
+ "revision": None,
140
+ # Hybrid-reasoning 8B (enable_thinking supported; routing passes it False).
141
+ # trust_remote_code custom modeling (sparse "InfLLM v2" attention) β€” same
142
+ # rot risk that broke MiniCPM3-4B on current transformers; pin a reviewed
143
+ # commit for any real use. NOTE: 8B / ~16 GiB bf16 does NOT fit the
144
+ # production find turn (the 8B didn't fit β€” VRAM logs); benchmarkable in
145
+ # THIS eval (brain-only load) to gauge the quality ceiling, but shipping it
146
+ # needs quantization (4-bit ~5-6 GiB) + a coexistence VRAM check.
147
+ "thinking": True,
148
+ "trust_remote_code": True,
149
+ },
150
+ {
151
+ "key": "minicpm4.1-8b-4bit",
152
+ "label": "MiniCPM4.1 8B (4-bit)",
153
+ "model_id": "openbmb/MiniCPM4.1-8B",
154
+ "revision": None,
155
+ "thinking": True,
156
+ "trust_remote_code": True,
157
+ # bitsandbytes nf4 (~5-6 GiB vs ~16 GiB bf16) to fit the 8B into the
158
+ # find-turn VRAM budget; quantized weights load straight onto the GPU, so
159
+ # device_map is set (skips the host→device .to copy). The question this
160
+ # answers: does 4-bit hold the bf16 8B's quality (0.87/0.86)?
161
+ "quantization": "4bit",
162
+ "device_map": {"": 0},
163
+ },
164
  ]
165
  DEFAULT_AGENT_MODEL = AGENT_MODELS[0]["key"]
166
  # A tool-call decision is short JSON; a rerank reply is a single number. 96 was
frontend/index.html CHANGED
@@ -261,11 +261,6 @@
261
  p. <span x-text="viewPage"></span><span class="text-brand-400 font-medium"> / <span x-text="docPages"></span></span>
262
  </div>
263
  <div class="flex-1 min-w-0 text-[12px] text-brand-500/90 truncate text-right pr-1" x-text="sectionTitle(viewPage)"></div>
264
- <button @click="spread=!spread" :aria-pressed="spread" aria-label="Two-page view" title="Two-page view"
265
- class="grid place-items-center w-11 h-11 rounded-xl transition"
266
- :class="spread ? 'text-brand-700 bg-brand-100' : 'text-brand-600 hover:bg-brand-100'">
267
- <i data-lucide="book-open" class="w-5 h-5"></i>
268
- </button>
269
  <button x-show="navStack.length" @click="goBack()" aria-label="Back" title="Back to previous spot"
270
  class="grid place-items-center w-11 h-11 rounded-xl text-brand-600 hover:bg-brand-100 transition">
271
  <i data-lucide="undo-2" class="w-5 h-5"></i>
@@ -273,8 +268,8 @@
273
  </div>
274
 
275
  <!-- the page(s): a book-style spread β€” the active page plus the next one
276
- for context (toggle to single with the book button). Each slot is a
277
- full-height flex column; the image fits the slot's HEIGHT and (in
278
  spread) at most half the width, so both facing pages stay fully
279
  visible without blowing up. img + svg are stacked in one grid cell
280
  and both carry the page's aspect-ratio + max constraints, so they
 
261
  p. <span x-text="viewPage"></span><span class="text-brand-400 font-medium"> / <span x-text="docPages"></span></span>
262
  </div>
263
  <div class="flex-1 min-w-0 text-[12px] text-brand-500/90 truncate text-right pr-1" x-text="sectionTitle(viewPage)"></div>
 
 
 
 
 
264
  <button x-show="navStack.length" @click="goBack()" aria-label="Back" title="Back to previous spot"
265
  class="grid place-items-center w-11 h-11 rounded-xl text-brand-600 hover:bg-brand-100 transition">
266
  <i data-lucide="undo-2" class="w-5 h-5"></i>
 
268
  </div>
269
 
270
  <!-- the page(s): a book-style spread β€” the active page plus the next one
271
+ for context (always two pages, except on the last page). Each slot is
272
+ a full-height flex column; the image fits the slot's HEIGHT and (in
273
  spread) at most half the width, so both facing pages stay fully
274
  visible without blowing up. img + svg are stacked in one grid cell
275
  and both carry the page's aspect-ratio + max constraints, so they
models/minicpm_agent.py CHANGED
@@ -88,6 +88,10 @@ SYSTEM_PROMPT = (
88
  'verb they use ("find", "search", "show", "where is", "circle") β€” if it is on '
89
  "the page in front of you, you point at it; you do NOT search for a better "
90
  "page, and you do NOT circle a different component than the one asked for.\n"
 
 
 
 
91
  "2. If it is NOT in the current page text, it is not on screen. Then:\n"
92
  " - A chapter, system, or section named by topic, or any part / procedure / "
93
  'spec you cannot see ("go to the cooling system", "engine oil capacity", '
@@ -218,9 +222,14 @@ def search_result_message(request: str, page: int, text: str, stuck: bool) -> st
218
  "- Only if it is truly not in this manual, use done."
219
  )
220
  return body + (
221
- f"The mechanic asked for: {request!r}. First, does THIS page show it β€” the "
222
- "part, or the line/value that answers it (it counts even when named inside "
223
- "a figure or diagram description)? If so, circle it now β€” set \"target\" to "
 
 
 
 
 
224
  f"a part name or the exact words printed on p.{page} (copy them from the "
225
  "page text above), NOT the mechanic's question: "
226
  '{"tool": "circle", "target": "<exact printed words for the part/value>", '
@@ -319,6 +328,25 @@ def use_model(key: str | None = None) -> str:
319
  device_map = spec.get("device_map")
320
  if device_map is not None:
321
  load_kwargs["device_map"] = device_map
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  model = AutoModelForCausalLM.from_pretrained(spec["model_id"], **load_kwargs)
323
  if device_map is None:
324
  model = model.to("cuda")
@@ -328,10 +356,18 @@ def use_model(key: str | None = None) -> str:
328
  return _active_key
329
 
330
 
331
- # Load the default brain eagerly at import β€” same as the other models, so the
332
- # ZeroGPU startup packing covers it and the common (no-switch) path pays no
333
- # load cost on the first turn.
334
- use_model(DEFAULT_AGENT_MODEL)
 
 
 
 
 
 
 
 
335
 
336
 
337
  def _template_kwargs() -> dict:
@@ -347,6 +383,12 @@ def _generate(
347
  """Greedy decode the assistant's next message. Traced as one `generation`
348
  (the resident brain as the model, the messages as input, the reply and the
349
  in/out token counts attached) when Langfuse is configured."""
 
 
 
 
 
 
350
  with tracing.generation(
351
  trace_name, model=_active_model_id(), input=messages
352
  ) as gen:
 
88
  'verb they use ("find", "search", "show", "where is", "circle") β€” if it is on '
89
  "the page in front of you, you point at it; you do NOT search for a better "
90
  "page, and you do NOT circle a different component than the one asked for.\n"
91
+ "If instead they asked to SEE or SHOW a whole diagram, overview, or components "
92
+ "view (not one specific part) and it is already on the screen, the PAGE ITSELF "
93
+ "is the answer β†’ reply done with a brief confirmation; do NOT circle one "
94
+ "component out of a diagram they asked to see in full.\n"
95
  "2. If it is NOT in the current page text, it is not on screen. Then:\n"
96
  " - A chapter, system, or section named by topic, or any part / procedure / "
97
  'spec you cannot see ("go to the cooling system", "engine oil capacity", '
 
222
  "- Only if it is truly not in this manual, use done."
223
  )
224
  return body + (
225
+ f"The mechanic asked for: {request!r}. First, is THIS the right page? Check "
226
+ "the title/section at the top: if the page is about a DIFFERENT system than "
227
+ 'they asked about β€” a part that merely shares a word (a "gear" inside a '
228
+ "fuel-system actuator is NOT a transmission gear) β€” it is the WRONG page, "
229
+ "so do NOT circle; search again with a more specific query. If it IS the "
230
+ "right page and shows the part, or the line/value that answers it (it "
231
+ "counts even when named inside a figure or diagram description), circle it "
232
+ 'now β€” set "target" to '
233
  f"a part name or the exact words printed on p.{page} (copy them from the "
234
  "page text above), NOT the mechanic's question: "
235
  '{"tool": "circle", "target": "<exact printed words for the part/value>", '
 
328
  device_map = spec.get("device_map")
329
  if device_map is not None:
330
  load_kwargs["device_map"] = device_map
331
+ # A spec can request on-the-fly bitsandbytes quantization (e.g. "4bit" to fit
332
+ # an 8B brain into the find-turn VRAM budget). Quantized weights are placed on
333
+ # the GPU at load time, so such a spec must also set device_map (skips the
334
+ # .to("cuda") below). bitsandbytes is imported lazily so non-quantized brains
335
+ # β€” and the production image, which omits it β€” never touch it.
336
+ quant = spec.get("quantization")
337
+ if quant in ("4bit", "8bit"):
338
+ from transformers import BitsAndBytesConfig
339
+
340
+ load_kwargs["quantization_config"] = (
341
+ BitsAndBytesConfig(
342
+ load_in_4bit=True,
343
+ bnb_4bit_quant_type="nf4",
344
+ bnb_4bit_compute_dtype=torch.bfloat16,
345
+ bnb_4bit_use_double_quant=True,
346
+ )
347
+ if quant == "4bit"
348
+ else BitsAndBytesConfig(load_in_8bit=True)
349
+ )
350
  model = AutoModelForCausalLM.from_pretrained(spec["model_id"], **load_kwargs)
351
  if device_map is None:
352
  model = model.to("cuda")
 
356
  return _active_key
357
 
358
 
359
+ # Load the default brain eagerly at import so ZeroGPU's startup tensor-packing
360
+ # covers it and the common (no-switch) first turn pays no load cost. EXCEPTION: a
361
+ # bitsandbytes-quantized default must NOT be built in the main process. Plain
362
+ # .to("cuda") models are safe at import because the `spaces` library patches torch
363
+ # and "packs" them into the forked GPU worker β€” but bitsandbytes initializes CUDA
364
+ # directly (bypassing that patch), which hard-errors on ZeroGPU ("CUDA must not be
365
+ # initialized in the main process") and crashes the Space at boot. So for a
366
+ # quantized default, DEFER the load to first GPU use: the pipeline calls use_model()
367
+ # inside its @spaces.GPU window (pipelines/agent_ask.py), and _generate() lazy-loads
368
+ # as a backstop β€” both on the GPU, the only supported place to build a bnb model.
369
+ if not _spec(DEFAULT_AGENT_MODEL).get("quantization"):
370
+ use_model(DEFAULT_AGENT_MODEL)
371
 
372
 
373
  def _template_kwargs() -> dict:
 
383
  """Greedy decode the assistant's next message. Traced as one `generation`
384
  (the resident brain as the model, the messages as input, the reply and the
385
  in/out token counts attached) when Langfuse is configured."""
386
+ # Backstop for a deferred (quantized) default brain: it isn't loaded at import
387
+ # on ZeroGPU, so build it on first use. Always reached inside a @spaces.GPU
388
+ # window (the pipeline's find turn / the eval's GPU fn) β€” the supported place to
389
+ # instantiate a bitsandbytes model. A no-op once a brain is resident.
390
+ if _MODEL is None:
391
+ use_model(DEFAULT_AGENT_MODEL)
392
  with tracing.generation(
393
  trace_name, model=_active_model_id(), input=messages
394
  ) as gen:
requirements.txt CHANGED
@@ -2,6 +2,7 @@ spaces
2
  gradio
3
  transformers>=4.57.2,<5
4
  accelerate
 
5
  torchvision
6
  pymupdf
7
  pillow
 
2
  gradio
3
  transformers>=4.57.2,<5
4
  accelerate
5
+ bitsandbytes # int8/4bit agent-brain quantization (default brain is int8 MiniCPM4.1-8B)
6
  torchvision
7
  pymupdf
8
  pillow