Skip to content

Serve self-hosted models on Modal from FACTORY (Qwen/H100, Nemotron/B300); release 0.5.0 - #14

Merged
flujo-app merged 2 commits into
mainfrom
selfhost-modal
Nov 3, 2026
Merged

flujo-app merged 2 commits into
mainfrom
selfhost-modal

Conversation

@flujo-app

Copy link
Copy Markdown
Owner

Apps that wanted their workers on a self-hosted model each carried their own vLLM-on-Modal deployment: settings, Modal app, discovery middleware, deploy and cleanup. FACTORY now provides it once, and its output plugs straight into the model provider from #10 and #13. Ships as 0.5.0.

selfhost/modal/ (separate from the earlier modal/ pilot code)

  • app.py: one vLLM web function per replica (1 to 4), each with at most one GPU container. The bearer key lives in a Modal secret, idle replicas scale down, and the settings are frozen into the image. CPU-only helpers prefetch weights and check vLLM flags; they exist only in short-lived runs, never in the deployed app.
  • settings.py: testable settings and profile merging, validation (GPU, replicas, scale-down, pinned image, variants, unknown fields), a memory estimate, and list-price estimates from prices.json (Modal per-second rates).
  • Profiles:
    • qwen: Qwen3.8-27B FP8 on one H100, about $4.84/h, native 262k context, parsers qwen3_xml / qwen3.
    • nemotron: Nemotron 3.5 Super VL 120B-A12B NVFP4 on one B300, about $8.43/h. It uses the vLLM 0.32.0 release image, pinned to its linux/amd64 digest.
    • Nemotron variants: base, fast, mtp1 (default), mtp3 and mtp1-fp16. All flags were checked against vLLM 0.32.0, the current stable release: multi-token prediction, async scheduling, the FlashInfer Mamba kernel, and hybrid prefix caching (--mamba-cache-mode align).
  • discovery.py: adds context length and capabilities to /v1/models, and reports text-only deployments honestly.
  • deploy.py: one JSON request in, one JSON line out (describe, prefetch, check-flags, deploy). Modal's output is discarded, so the key and transcripts never reach the caller.

ModalSelfHostedModel (SDK) and factory-selfhost (CLI: profiles | deploy | status | endpoint | key | verify | stop | retire)

  • Owned app names (<prefix>-<profile>-<12 hex>), a private journal, and a fresh key held only by the Modal secret and the caller's credential store.
  • Proof that each replica serves the expected model: the catalogue lists it, and one non-thinking completion returns an exact challenge. Modal's 303 result links are followed on the same origin only; the POST is never repeated and the key never goes elsewhere. Bodies are capped.
  • Any uncertain remote outcome becomes unknown and is never retried automatically. stop (GPU off, keep key and weights, redeploy later) and retire (delete app, secret, both volumes and key, then check) refuse a changed Modal workspace.
  • endpoint() returns a validated model provider for cloud lite workers (Codex) and local lite workers (Codex or Claude Code). adopt() takes over an owned app from an earlier version so it can be retired.

Docs: selfhost/README.md (profiles, prices, measured agent capacity, usage) and PUBLIC_SDK.md. The package now ships the Python sources and profiles (not their tests) and the factory-selfhost bin.

Fixes #12

Local CI (AGENTS.md)

Same steps as .github/workflows/public-sdk.yml (new: the Python unittest step, the selfhost test file, factory-selfhost profiles from the installed package), on a clean git archive export of 591a4d3 with fresh npm ci:

Platform Node / npm prod audit (high) Focused tests (24 files) Python settings tests Packed install + verify-public-sdk CLI help + factory-selfhost profiles Installed MCP stdio Evidence regressions
Windows 10, portable Node, Python 3.13 22.17.0 / 10.9.2 0 vulnerabilities 198/198 9/9 ok ok 1/1 7/7
Windows 10, portable Node, Python 3.13 24.22.2 / 11.19.1 0 vulnerabilities 198/198 9/9 ok ok 1/1 7/7
Linux, Docker node:22.17.0-bookworm, Python 3.11 22.17.0 / 10.9.2 0 vulnerabilities 198/198 9/9 ok ok 1/1 7/7
Linux, Docker node:24.22.2-bookworm, Python 3.11 24.22.2 / 11.19.1 0 vulnerabilities 198/198 9/9 ok ok 1/1 7/7

No Modal deployment, GPU or model call was made. The measured numbers in the docs come from earlier recorded runs.

Apps carried their own copies of a vLLM-on-Modal deployment. FACTORY now
provides it once:

- selfhost/modal: Modal app (one web function per replica, at most one
  container each, bearer key in a Modal secret, idle scale-down, CPU-only
  weight prefetch and flag check), testable settings with Qwen3.8-27B
  (H100) and Nemotron 3.5 Super VL NVFP4 (B300) profiles, discovery
  middleware, and a deploy script that never prints the key
- ModalSelfHostedModel: owned app names, private journal, verified model
  probe that follows Modal 303 results on the same origin only, stop,
  redeploy and retire with checked cleanup; endpoint() returns a model
  provider for cloud and local workers
- factory-selfhost CLI: profiles, deploy, status, endpoint, key, verify,
  stop, retire
- Python and Node offline tests, packaged sources, docs; version 0.5.0

Fixes #12.
An app that created its owned Modal model before using FACTORY can hand
it over: adopt() records it as unknown (never trusted for work) so that
retire() stops it and checks that its secret and volumes are gone.
@flujo-app flujo-app added documentation Improvements or additions to documentation enhancement New feature or request labels Nov 3, 2026
@flujo-app
flujo-app merged commit 52428ba into main Nov 3, 2026
2 checks passed
@flujo-app
flujo-app deleted the selfhost-modal branch November 3, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-hosted model serving on Modal inside FACTORY (Qwen/H100 and Nemotron/B300 profiles)

1 participant