| # Modal Inference Backend |
|
|
| Token Hold'em keeps Gradio and deterministic poker logic in the local or Hugging Face Space process. Modal owns model decisions when `USE_MODAL_INFERENCE=true`. |
|
|
| Unlike the early prototype, model-enabled play no longer silently falls back to deterministic persona bots. If Modal is unavailable, the model returns invalid JSON, a model is disabled, or a gated model cannot be accessed, the app logs the failure and surfaces a clear unavailable message without applying a fake action. |
|
|
| ## Setup |
|
|
| Install project dependencies: |
|
|
| ```bash |
| uv sync |
| ``` |
|
|
| Authenticate Modal: |
|
|
| ```bash |
| modal setup |
| ``` |
|
|
| Create the Modal Hugging Face secret from the repo `.env` file: |
|
|
| ```bash |
| uv run modal secret create token-holdem-hf-token --from-dotenv .env --force |
| ``` |
|
|
| ## Deploy Modal |
|
|
| ```bash |
| uv run modal deploy modal_inference.py |
| ``` |
|
|
| Pre-download the enabled model snapshots into the Modal Volume. This fans out one Modal setup call per enabled model: |
|
|
| ```bash |
| uv run modal run modal_inference.py::setup_cache |
| ``` |
|
|
| Warm the deployed demo workers before recording or judging. This starts each enabled model worker in parallel: |
|
|
| ```bash |
| uv run modal run modal_inference.py::warmup_demo |
| ``` |
|
|
| Run a smoke test without the Gradio UI: |
|
|
| ```bash |
| uv run modal run modal_inference.py::smoke --model-name Gemma |
| ``` |
|
|
| Print one smoke command per enabled model: |
|
|
| ```bash |
| uv run python scripts/modal_smoke_enabled_models.py |
| ``` |
|
|
| Run those smoke checks: |
|
|
| ```bash |
| uv run python scripts/modal_smoke_enabled_models.py --run |
| ``` |
|
|
| ## Run Gradio With Modal |
|
|
| ```bash |
| USE_MODAL_INFERENCE=true uv run python app.py |
| ``` |
|
|
| The default `TOKEN_HOLDEM_MODAL_MODEL_NAMES` behavior enables the runtime-feasible roster, including Cohere Command R7B through Transformers. |
|
|
| ## Runtime Coverage |
|
|
| - GGUF seats (`Nemotron Nano`) route to `GgufModelWorker`, a parameterized Modal class that loads one GGUF with `llama.cpp` during `@modal.enter`. |
| - Gemma routes to `MultimodalModelWorker`, a parameterized Modal class that loads one multimodal Transformers model during `@modal.enter`. |
| - Standard Transformers seats (`Qwen`, `Cohere Command R7B`, `Mistral`, `Llama Scout`) route to `CausalModelWorker`, a parameterized Modal class that loads one causal-LM model during `@modal.enter`. |
| - The Modal Volume `token-holdem-hf-cache` is mounted at `/cache/huggingface`. `HF_HOME`, `TRANSFORMERS_CACHE`, `HF_HUB_CACHE`, and `HUGGINGFACE_HUB_CACHE` point at that mount, so Hugging Face downloads survive container scale-down. |
| - `setup_cache` uses one parallel `snapshot_download` call per enabled model to populate the mounted Volume before the demo. GGUF repos are restricted to the configured quantized file instead of pulling every quantization. |
| - `warmup_demo` calls each parameterized worker's `warmup` method in parallel, which runs the same `@modal.enter` model load path used by live decisions. Warm containers keep the loaded model/tokenizer or model/processor objects in process memory until Modal scales them down. |
| - `OpenAI Open Model 20B` routes to `HeavyCausalModelWorker`, which defaults to a larger `A100-80GB` GPU and pins `kernels==0.12.0` for MXFP4 quantized loading. Newer `kernels` releases changed `LayerRepository` construction and broke Transformers imports during warmup. |
| - `Qwen` and `Mistral` use public safetensors checkpoints through the Transformers path; their earlier GGUF routes were slower in live Modal validation. |
| - Modal workers generate and parse the poker action JSON only. Public table talk uses deterministic tavern templates after a valid action to avoid prompt leakage and remove a second model generation from each turn. |
| - `Llama Scout` uses `TinyLlama/TinyLlama-1.1B-Chat-v1.0`; the previously tested Meta Llama 3.2 checkpoint is gated and failed Modal cache setup without token access. |
| - North Mini was tested as `unsloth/North-Mini-Code-1.0-GGUF` with `North-Mini-Code-1.0-UD-Q4_K_M.gguf`, but a live Modal smoke test failed during `llama_cpp.Llama(...)` model load under the current runtime. North Mini FP8 loaded but failed on FP8 matmul support in the current Torch path. The active Cohere seat uses `CohereLabs/c4ai-command-r7b-12-2024` through Transformers. |
|
|
| ## Environment Variables |
|
|
| - `USE_MODAL_INFERENCE`: set to `true`, `1`, `yes`, or `on` to use `ModalRuntime`. |
| - `TOKEN_HOLDEM_MODAL_APP_NAME`: deployed Modal app name. Default: `token-holdem-inference`. |
| - `TOKEN_HOLDEM_MODAL_MODEL_NAMES`: comma-separated model/player names, `default`, or explicit `all`. Default enables all runtime-feasible seats. |
| - `TOKEN_HOLDEM_MODAL_HF_SECRET_NAME`: Modal secret name that exposes `HF_TOKEN`. Default: `token-holdem-hf-token`. |
| - `TOKEN_HOLDEM_MODAL_TIMEOUT_SECONDS`: local wait timeout and Modal worker timeout. Default: `300`. |
| - `TOKEN_HOLDEM_MODAL_DEMO_MODE`: keep demo defaults warm for longer. Default: `true`. |
| - `TOKEN_HOLDEM_MODAL_SCALEDOWN_SECONDS`: idle seconds to keep warm Modal workers alive. Default: `1800` in demo mode, `600` otherwise. |
| - `TOKEN_HOLDEM_MODAL_MIN_CONTAINERS`: optional always-warm worker count. Default: `0`; leave unset for cost-conscious demos. |
| - `TOKEN_HOLDEM_MODAL_GPU`: Modal GPU type for remote workers. Default: `L40S`; set empty to request no GPU. |
| - `TOKEN_HOLDEM_MODAL_HEAVY_GPU`: Modal GPU type for `HeavyCausalModelWorker`, currently the OpenAI 20B seat. Default: `A100-80GB`. |
| - `TOKEN_HOLDEM_MODEL_CACHE_DIR`: mounted Hugging Face cache root. Default: `/cache/huggingface`. |
| - `TOKEN_HOLDEM_GGUF_CONTEXT`: llama.cpp context length for Modal GGUF seats. Default: `4096`. |
| - `TOKEN_HOLDEM_GGUF_GPU_LAYERS`: llama.cpp GPU layer count. Default: `-1`. |
| - `TOKEN_HOLDEM_GGUF_DECISION_TOKENS`: max tokens for GGUF decision JSON. Default: `96`. |
| - `TOKEN_HOLDEM_GGUF_TALK_TOKENS`: max tokens for GGUF table talk. Default: `24`. |
| - `TOKEN_HOLDEM_ALLOW_MODEL_DOWNLOADS`: local-only flag for `LocalRuntime`. |
| - `TOKEN_HOLDEM_ALLOW_DETERMINISTIC_BOTS`: explicit development/test fallback mode. |
|
|
| ## Runtime Boundary |
|
|
| The boundary is `InferenceRuntime.decide(profile, state_summary)`. |
|
|
| The local Gradio process owns game creation, legal actions, betting repair, pot movement, showdown, leaderboard updates, event callbacks, and rendering. |
|
|
| Modal receives current game state, model/player name, persona, model id, legal action metadata, and the decision prompt. |
|
|
| Modal returns action, bet amount, explanation, commentary, raw model output, and an error field. The local adapter validates the response before the poker engine applies anything. |
|
|
| Disabled models and Modal failures are surfaced as model-unavailable errors in the UI and structured logs. The app does not apply deterministic bot actions for model seats unless `TOKEN_HOLDEM_ALLOW_DETERMINISTIC_BOTS=1` is explicitly used outside Modal mode. |
|
|
| Modal container logs include structured JSON timing rows for container start, snapshot download/cache-hit state, tokenizer or processor load, model load to GPU, first token time, total generation time, and cache commits. Worker `@modal.enter` model-loading errors are not converted into successful decisions; they fail the worker load and surface through Modal/local diagnostics. |
|
|