Spaces:
Running on Zero
Running on Zero
fix: batch quest analysis on zerogpu
Browse filesSync GitHub commit 45ef859; split MiniCPM quest refresh into smaller ZeroGPU batches.
- README.md +1 -0
- app.py +29 -1
- 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",
|