JacobLinCool commited on
Commit
2de9f4c
·
verified ·
1 Parent(s): 742999b

fix: batch quest analysis on zerogpu

Browse files

Sync GitHub commit 45ef859; split MiniCPM quest refresh into smaller ZeroGPU batches.

Files changed (3) hide show
  1. README.md +1 -0
  2. app.py +29 -1
  3. tests/test_app.py +42 -0
README.md CHANGED
@@ -219,6 +219,7 @@ ADVISOR_ADAPTER_ID=build-small-hackathon/hackathon-advisor-minicpm5-lora
219
  ADVISOR_ADAPTER_REVISION=25de69bcde397e1bcdd852923b56a42f10222650
220
  ADVISOR_QUEST_ANALYZER_BACKEND=minicpm-transformers
221
  ADVISOR_QUEST_ADAPTER_ID=artifacts/quest-lora
 
222
  ADVISOR_CACHE_DIR=/data/advisor-cache
223
  ADVISOR_REFRESH_EMBEDDING_TIMEOUT_SECONDS=1800
224
  ADVISOR_EMBEDDING_MODEL_REPO=ggml-org/embeddinggemma-300m-qat-q8_0-GGUF
 
219
  ADVISOR_ADAPTER_REVISION=25de69bcde397e1bcdd852923b56a42f10222650
220
  ADVISOR_QUEST_ANALYZER_BACKEND=minicpm-transformers
221
  ADVISOR_QUEST_ADAPTER_ID=artifacts/quest-lora
222
+ ADVISOR_QUEST_ANALYSIS_BATCH_SIZE=24
223
  ADVISOR_CACHE_DIR=/data/advisor-cache
224
  ADVISOR_REFRESH_EMBEDDING_TIMEOUT_SECONDS=1800
225
  ADVISOR_EMBEDDING_MODEL_REPO=ggml-org/embeddinggemma-300m-qat-q8_0-GGUF
app.py CHANGED
@@ -63,6 +63,7 @@ MAX_AUDIO_UPLOAD_BYTES = 25 * 1024 * 1024
63
  AUDIO_UPLOAD_SUFFIXES = {".aac", ".aif", ".aiff", ".flac", ".m4a", ".mp3", ".oga", ".ogg", ".opus", ".wav", ".webm"}
64
  DEFAULT_HF_ORG = "build-small-hackathon"
65
  DEFAULT_REFRESH_EMBEDDING_TIMEOUT_SECONDS = 1800
 
66
  REFRESH_SUBPROCESS_LOG_TAIL_LINES = 80
67
  REFRESH_STAGE_LABELS = {
68
  "crawling": "Fetching public Spaces",
@@ -132,7 +133,6 @@ def _transcribe_voice(audio_path: str) -> dict[str, Any]:
132
  return voice_transcriber.transcribe(Path(audio_path)).to_dict()
133
 
134
 
135
- @gpu_task
136
  def _analyze_dashboard_quests(project_rows: list[dict[str, Any]]) -> dict[str, Any]:
137
  missing_evidence_keys = [
138
  str(item.get("id") or index)
@@ -145,6 +145,24 @@ def _analyze_dashboard_quests(project_rows: list[dict[str, Any]]) -> dict[str, A
145
  f"missing evidence keys for {len(missing_evidence_keys)} projects"
146
  )
147
  projects = [Project.from_dict(item) for item in project_rows]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  analyzer = create_quest_analyzer(device="cuda" if zero_gpu_enabled() else "local")
149
  matches = analyzer.analyze(projects)
150
  source = getattr(analyzer, "source", "quest-analyzer")
@@ -155,6 +173,16 @@ def _analyze_dashboard_quests(project_rows: list[dict[str, Any]]) -> dict[str, A
155
  }
156
 
157
 
 
 
 
 
 
 
 
 
 
 
158
  def _refresh_public_state() -> dict[str, Any]:
159
  with _refresh_lock:
160
  return dict(_refresh_state)
 
63
  AUDIO_UPLOAD_SUFFIXES = {".aac", ".aif", ".aiff", ".flac", ".m4a", ".mp3", ".oga", ".ogg", ".opus", ".wav", ".webm"}
64
  DEFAULT_HF_ORG = "build-small-hackathon"
65
  DEFAULT_REFRESH_EMBEDDING_TIMEOUT_SECONDS = 1800
66
+ DEFAULT_QUEST_ANALYSIS_BATCH_SIZE = 24
67
  REFRESH_SUBPROCESS_LOG_TAIL_LINES = 80
68
  REFRESH_STAGE_LABELS = {
69
  "crawling": "Fetching public Spaces",
 
133
  return voice_transcriber.transcribe(Path(audio_path)).to_dict()
134
 
135
 
 
136
  def _analyze_dashboard_quests(project_rows: list[dict[str, Any]]) -> dict[str, Any]:
137
  missing_evidence_keys = [
138
  str(item.get("id") or index)
 
145
  f"missing evidence keys for {len(missing_evidence_keys)} projects"
146
  )
147
  projects = [Project.from_dict(item) for item in project_rows]
148
+ matches_by_project: dict[str, list[dict[str, Any]]] = {}
149
+ source = "quest-analyzer"
150
+ batch_size = _quest_analysis_batch_size()
151
+ for start in range(0, len(project_rows), batch_size):
152
+ batch_rows = project_rows[start : start + batch_size]
153
+ result = _analyze_dashboard_quest_batch(batch_rows)
154
+ source = str(result["source"])
155
+ matches_by_project.update(result["matches_by_project"])
156
+ validated = validate_matches_by_project(matches_by_project, projects, source=source)
157
+ return {
158
+ "source": validated.source,
159
+ "matches_by_project": validated.matches_by_project,
160
+ }
161
+
162
+
163
+ @gpu_task
164
+ def _analyze_dashboard_quest_batch(project_rows: list[dict[str, Any]]) -> dict[str, Any]:
165
+ projects = [Project.from_dict(item) for item in project_rows]
166
  analyzer = create_quest_analyzer(device="cuda" if zero_gpu_enabled() else "local")
167
  matches = analyzer.analyze(projects)
168
  source = getattr(analyzer, "source", "quest-analyzer")
 
173
  }
174
 
175
 
176
+ def _quest_analysis_batch_size() -> int:
177
+ raw = os.environ.get("ADVISOR_QUEST_ANALYSIS_BATCH_SIZE", "").strip()
178
+ if not raw:
179
+ return DEFAULT_QUEST_ANALYSIS_BATCH_SIZE
180
+ batch_size = int(raw)
181
+ if batch_size <= 0:
182
+ raise RuntimeError("ADVISOR_QUEST_ANALYSIS_BATCH_SIZE must be a positive integer.")
183
+ return batch_size
184
+
185
+
186
  def _refresh_public_state() -> dict[str, Any]:
187
  with _refresh_lock:
188
  return dict(_refresh_state)
tests/test_app.py CHANGED
@@ -290,6 +290,48 @@ def test_dashboard_refresh_quest_analysis_uses_minicpm_analyzer(monkeypatch) ->
290
  assert quests == {"Off the Grid", "Field Notes"}
291
 
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  def test_dashboard_refresh_quest_analysis_requires_two_segment_snapshot() -> None:
294
  project = Project(
295
  id="build-small-hackathon/missing-evidence",
 
290
  assert quests == {"Off the Grid", "Field Notes"}
291
 
292
 
293
+ def test_dashboard_refresh_quest_analysis_batches_minicpm(monkeypatch) -> None:
294
+ projects = [
295
+ Project(
296
+ id=f"build-small-hackathon/batched-{index}",
297
+ title=f"Batched {index}",
298
+ summary="Small local demo",
299
+ tags=("gradio",),
300
+ models=(),
301
+ datasets=(),
302
+ likes=0,
303
+ sdk="gradio",
304
+ license="mit",
305
+ created_at="2026-06-01T00:00:00+00:00",
306
+ last_modified="2026-06-08T00:00:00+00:00",
307
+ host=f"https://batched-{index}.hf.space",
308
+ url=f"https://huggingface.co/spaces/build-small-hackathon/batched-{index}",
309
+ readme_body="README evidence",
310
+ app_file_source="import gradio as gr",
311
+ )
312
+ for index in range(3)
313
+ ]
314
+ calls = []
315
+
316
+ class FakeMiniCPMAnalyzer:
317
+ source = "minicpm-json-quest-analyzer"
318
+
319
+ def analyze(self, batch):
320
+ calls.append([project.id for project in batch])
321
+ return {project.id: [] for project in batch}
322
+
323
+ monkeypatch.setenv("ADVISOR_QUEST_ANALYSIS_BATCH_SIZE", "2")
324
+ monkeypatch.setattr(app_module, "create_quest_analyzer", lambda device: FakeMiniCPMAnalyzer())
325
+
326
+ result = app_module._analyze_dashboard_quests([project.to_refresh_snapshot_dict() for project in projects])
327
+
328
+ assert calls == [
329
+ ["build-small-hackathon/batched-0", "build-small-hackathon/batched-1"],
330
+ ["build-small-hackathon/batched-2"],
331
+ ]
332
+ assert set(result["matches_by_project"]) == {project.id for project in projects}
333
+
334
+
335
  def test_dashboard_refresh_quest_analysis_requires_two_segment_snapshot() -> None:
336
  project = Project(
337
  id="build-small-hackathon/missing-evidence",