svlm-council / CLAUDE.md
John Ho
Set an explicit queue max_size=16 for ZeroGPU
5532583
|
Raw
History Blame Contribute Delete
7.58 kB

A newer version of the Gradio SDK is available: 6.27.0

Upgrade

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A Gradio app deployed to a Hugging Face Space (ZeroGPU), with dependencies managed by uv and continuous deployment via GitHub Actions. The app (app.py) serves the sVLM-Council β€” small (~1–9B) vision-language models answering the same image+prompt side by side β€” as a UI, REST API, and MCP server. Space: GF-John/svlm-council.

Model comparison scripts

Alongside the template app.py, the repo has three self-contained PEP 723 uv scripts: ocr_arena.py (full-image OCR across backends), extract_arena.py (targeted field extraction with N/A-abstention guardrails and -F format directives), and svlm_council.py (small VLMs run locally via transformers; importable query() used by extract_arena). Each declares its own deps inline β€” run with uv run [--env-file .env] <script>.

When adding, replacing, or re-evaluating models, start from MODEL_RESEARCH.md β€” it holds the verified candidate tables (hosted + local), benchmark snapshots, provider-reliability notes, per-model transformers quirks, prompting findings, and verified model licenses (with per-scenario implications), plus the endpoints to re-check since that data goes stale. Update it after any new model research.

Commands

  • Install deps: uv sync (creates/updates .venv from pyproject.toml + uv.lock)
  • Add a dependency: uv add <package> (updates pyproject.toml and uv.lock)
  • Run the app locally: uv run app.py (launches Gradio on http://127.0.0.1:7860, with Swagger docs at /docs and an MCP server enabled)
  • requirements.txt is hand-curated (loose pins; no gradio/spaces β€” the Space's SDK provides those). Don't regenerate it with uv export; when app code gains a dependency, append it manually.

There are no tests or linters configured.

Deployment

Push to main triggers .github/workflows/deploy_to_hf_space.yaml, which:

  1. Generates requirements.txt from pyproject.toml via uv export only when it doesn't already exist (the Space itself has no pyproject.toml; HF Spaces read requirements.txt), then commits it back.
  2. Pushes the repo to the HF Space β€” a normal push by default, or a force push if the FORCE_PUSH secret is set (a force push overwrites the Space's history, so use it only if you are the sole contributor).

Configuration is driven entirely by GitHub secrets (no workflow edits needed):

  • HF_TOKEN (required) β€” auth token. If absent the push step is skipped (the workflow still succeeds).
  • HF_USERNAME and SPACE_NAME (required) β€” interpolated into the push URL .../spaces/$HF_USERNAME/$SPACE_NAME.
  • FORCE_PUSH (optional) β€” any value enables force push.

Also update the YAML frontmatter in README.md (title, emoji, sdk_version, short_description, etc.) β€” HF Spaces reads its config from there.

Binary assets

HF Spaces rejects raw binaries in the git tree β€” image/video/audio/model-weight files must go through Git LFS / Xet (git lfs track), not be committed directly. Common binary extensions are git-ignored in .gitignore to prevent accidental commits; if a binary genuinely needs to ship, track it with LFS rather than removing the ignore rule.

App architecture (app.py)

app.py is a gr.TabbedInterface with two tabs, each a gr.Interface = REST endpoint = MCP tool: Council (council_query) serves the council from svlm_council.py β€” it imports the COUNCIL registry from there but has its own ZeroGPU load/infer path (the CLI's MPS single-slot eviction cache doesn't apply on the Space). Adding/changing a council member means editing COUNCIL in svlm_council.py; app.py picks it up automatically. ExtractArena (extract_fields) reuses extract_arena.py as a library: its prompt building (build_prompt), reply normalization (_normalize_value/_parse_json_reply), and remote backends (extract_arena.MODELS[name].fn β€” HF Inference Providers + the moondream Space) run verbatim in the main process (unbilled); local council names instead route through the resident-model _gpu_run_council path (never extract_arena's MPS loader). import extract_arena must stay after _load_dotenv() β€” it reads HF_TOKEN at import time. HF_TOKEN is optional: without it the hosted backends fail per-model (logged) while the rest run. Result shape mirrors ExtractResult: {"model", "value", "values", "raw", "latency_s", "error"} (value normalized single-field answer with "" = abstained; values for multi-field). Extract knobs: MAX_EXTRACT_MODELS (default 3), DEFAULT_EXTRACT_MODELS (default qwen3.5,muse-glimmer).

  • Startup (module level, unbilled): every enabled member is loaded via AutoProcessor/AutoModelForImageTextToText and moved .to("cuda") β€” on ZeroGPU the main process runs a CUDA emulation mode, so module-level CUDA placement is the officially recommended pattern (lazy-loading inside @spaces.GPU is discouraged). All four members β‰ˆ 37GB bf16, fitting the default large ZeroGPU slice (48GB VRAM; xlarge = 96GB at 2Γ— quota). No flash-attn and no quantization; on CUDA every member is forced to sdpa attention (a spec'd eager β€” hunyuan-ocr β€” would materialize seqΒ² float32 attention transients that OOM the VRAM left beside the resident council).
  • council_query is the public function = REST endpoint = MCP tool. It validates (returns a structured single-element error list instead of raising, so MCP clients get something actionable), then delegates to _gpu_run_council, the only @spaces.GPU function β€” its dynamic duration callable scales with the number of selected models (the requested duration is ZeroGPU's quota gate, so keep it tight; tune from Space logs).
  • Result shape: list[dict], one per model: {"model", "text", "latency_s", "error"}; per-model failures are isolated.
  • Concurrency: no locking in the code; Gradio's queue does it. concurrency_limit=2 on the Council interface and 4 on ExtractArena (each concurrent _gpu_run_council call forks its own ZeroGPU worker and GPU, so the ceiling is the GPU pool, not VRAM; remote extract backends are I/O-bound and overlap freely). app.queue(max_size=16) is explicit because ZeroGPU forces max_size=1 when unset, which 503s ("Queue is full") every caller beyond the running six plus one waiter. Within one request models run sequentially.
  • MCP docstring is assigned to council_query.__doc__ after the def (f-string β€” it embeds the runtime roster, max, and the MODEL_RESEARCH.md links so calling agents can pick models). It follows the gradio-mcp-tool-docstrings skill rules: schema + URLs in the description prose, one-line Args: entries, no Returns: section. Re-verify with the skill's dump_mcp_schema.py after editing it.
  • Env knobs: MAX_COUNCIL_MODELS (default 3), DEFAULT_COUNCIL_MODEL (default qianfan-ocr), COUNCIL_MEMBERS (comma-separated subset to load; COUNCIL_MEMBERS=hunyuan-ocr uv run app.py is the lightweight local dev run β€” the full council won't fit in 32GB local RAM), MAX_IMAGE_PIXELS (default 3,000,000 β€” inputs above this are downscaled in the main process before inference; hunyuan-ocr's processor has no resolution cap of its own, and its eager attention OOMs on huge uploads without this guard).
  • Version floors: torch is pinned >=2.10,<2.12 because ZeroGPU only supports torch up to 2.11; gradio is 6.x (gradio[mcp] extra required for mcp_server=True).