# Modal apps These files define separate Modal deployments for the virtual character project. ## Setup ```powershell python -m pip install -r requirements.txt modal setup modal secret create hf-token HF_TOKEN=hf_xxx ``` You must also accept gated model licenses on Hugging Face before Modal can download those weights. If your Modal Secret uses a different name, set it before running checks or deploys: ```powershell $env:VC_HF_SECRET_NAME="your-secret-name" ``` ## First checks Run remote method health checks: ```powershell modal run modal_apps/modal_ping.py modal run modal_apps/modal_hf_check.py python scripts/check_modal_connectivity.py --mode remote-methods ``` `modal_ping.py` is CPU-only and only checks login/connectivity. `modal_hf_check.py` is also CPU-only and checks whether Modal can read `hf-token` and query the selected Hugging Face model metadata without downloading weights. The health checks in `check_modal_connectivity.py` start the service containers but do not load model weights. Actual generation tests will load models and consume GPU credits. Benchmark Gemma on Modal: ```powershell $env:VC_BENCH_MODEL="google/gemma-4-12B-it" $env:VC_BENCH_GPU="L40S" modal run modal_apps/modal_gemma_benchmark.py --max-new-tokens 64 ``` Benchmark Gemma through vLLM: ```powershell $env:VC_VLLM_MODEL="google/gemma-4-12B-it" $env:VC_VLLM_VERSION="0.22.1" $env:VC_VLLM_GPU="L40S" $env:VC_VLLM_FAST_BOOT="1" modal run modal_apps/modal_vllm_gemma.py --max-tokens 128 ``` Note: `vllm==0.22.1` is the latest PyPI stable release checked on 2026-06-12, but it still does not run `google/gemma-4-12B-it` correctly in our Modal test. For Gemma 4 12B, use this only as a regression check. Benchmark Gemma through vLLM nightly: ```powershell $env:PYTHONIOENCODING="utf-8" $env:PYTHONUTF8="1" $env:VC_SKIP_HF_SECRET="1" $env:VC_VLLM_MODEL="google/gemma-4-12B-it" $env:VC_VLLM_PACKAGE="vllm==0.22.1rc1.dev468+gfbc3a1907.cu129" $env:VC_VLLM_EXTRA_INDEX_URL="https://wheels.vllm.ai/nightly/cu129" $env:VC_VLLM_UV_EXTRA_OPTIONS="--index-strategy unsafe-best-match" $env:VC_VLLM_PRE="1" $env:VC_VLLM_GPU="L40S" $env:VC_VLLM_FAST_BOOT="1" modal run modal_apps/modal_vllm_gemma.py --max-tokens 128 ``` Deploy the working vLLM nightly endpoint: ```powershell $env:PYTHONIOENCODING="utf-8" $env:PYTHONUTF8="1" $env:VC_SKIP_HF_SECRET="1" $env:VC_VLLM_MODEL="google/gemma-4-12B-it" $env:VC_VLLM_PACKAGE="vllm==0.22.1rc1.dev468+gfbc3a1907.cu129" $env:VC_VLLM_EXTRA_INDEX_URL="https://wheels.vllm.ai/nightly/cu129" $env:VC_VLLM_UV_EXTRA_OPTIONS="--index-strategy unsafe-best-match" $env:VC_VLLM_PRE="1" $env:VC_VLLM_GPU="L40S" $env:VC_VLLM_FAST_BOOT="1" modal deploy modal_apps/modal_vllm_gemma.py ``` Keep the deployed vLLM endpoint warm: ```powershell python scripts/set_modal_vllm_autoscaler.py on ``` This updates the deployed `serve` function to `min_containers=1`, `buffer_containers=0`, and `scaledown_window=1200`. It takes effect without rebuilding the image, but Modal resets this override on the next deploy. To make warm residency part of the deployment configuration, set these before `modal deploy`: ```powershell $env:VC_VLLM_MIN_CONTAINERS="1" $env:VC_VLLM_BUFFER_CONTAINERS="0" $env:VC_VLLM_SCALEDOWN_WINDOW="1200" modal deploy modal_apps/modal_vllm_gemma.py ``` Turn warm residency off when the demo window is over: ```powershell python scripts/set_modal_vllm_autoscaler.py off ``` Current deployed endpoint: ```text https://veronicaulises0--virtual-characters-vllm-gemma-serve.modal.run ``` This deployment intentionally skips mounting `hf-token` into the nightly vLLM runtime. It depends on the `vc-hf-cache` Modal Volume already containing `google/gemma-4-12B-it`. To avoid building every service image at once, check one service at a time: ```powershell python scripts/check_modal_connectivity.py --mode remote-methods --service llm python scripts/check_modal_connectivity.py --mode remote-methods --service tts python scripts/check_modal_connectivity.py --mode remote-methods --service image ``` ## Character generation spike The automated character-generation spike is intentionally isolated from the Gradio UI and the deployed image endpoint. Safe checks: ```powershell python scripts/run_character_generation_spike.py list-models python scripts/run_character_generation_spike.py modal-health ``` `modal-health` starts the Modal app but does not load model weights. Generation probes load weights and consume GPU credits, so the CLI requires `--confirm-gpu`: ```powershell python scripts/run_character_generation_spike.py modal-probe --candidate flux_schnell --batch-size 1 --confirm-gpu python scripts/run_character_generation_spike.py modal-benchmark --candidates flux_schnell qwen_image --confirm-gpu python scripts/run_character_generation_spike.py modal-benchmark --candidates qwen_image_edit --init-image path\to\reference.png --include-expressions --confirm-gpu python scripts/run_character_generation_spike.py modal-benchmark --candidates qwen_controlnet_union --control-image path\to\pose.png --include-expressions --confirm-gpu ``` Candidates: - `flux_schnell`: speed baseline using `black-forest-labs/FLUX.1-schnell`. - `qwen_image`: Chinese prompt and high-quality text-to-image candidate using `Qwen/Qwen-Image`. - `qwen_image_edit`: expression/edit candidate using `Qwen/Qwen-Image-Edit`. - `qwen_controlnet_union`: pose/canny/depth action candidate using `InstantX/Qwen-Image-ControlNet-Union`. - `instantid_sdxl`: tracked as identity-preserving candidate, but disabled until the face-analysis/model download path is pinned. The spike image installs diffusers from GitHub because the Qwen Image and ControlNet pipelines require recent upstream support. If a model is gated or not cached, make sure the `hf-token` secret has accepted the Hugging Face model terms. If `hf-token` is not created yet and you only want to verify Modal container startup, use: ```powershell $env:VC_SKIP_HF_SECRET="1" python scripts/check_modal_connectivity.py --mode remote-methods ``` Only deploy with `VC_SKIP_HF_SECRET=1` when the selected model is public or the required weights are already cached in the mounted Modal Volume. The working vLLM Gemma 4 deployment uses this cache-based route to avoid exposing `hf-token` to a nightly dependency stack. Deploy services: ```powershell modal deploy modal_apps/modal_llm.py modal deploy modal_apps/modal_tts.py modal deploy modal_apps/modal_image.py ``` Then set endpoint URLs from Modal output: ```powershell $env:VC_MODAL_LLM_URL="https://...modal.run/persona_events" $env:VC_MODAL_TTS_URL="https://...modal.run/tts" $env:VC_MODAL_IMAGE_URL="https://...modal.run/character_image" python scripts/check_modal_connectivity.py --mode endpoints ``` ## Cost defaults - vLLM LLM: `google/gemma-4-12B-it`, GPU `L40S`, scaledown 5 min unless `VC_VLLM_MIN_CONTAINERS=1`. - Transformers LLM fallback: `google/gemma-4-12B-it`, GPU `L40S`, scaledown 3 min. - TTS: Chatterbox, GPU `A10G`, scaledown 3 min. - Image: FLUX.1-schnell, GPU `H100`, scaledown 1 min. Modal public GPU prices checked on 2026-06-14: | GPU | Approx hourly | Approx 7 days | | --- | ---: | ---: | | T4 | $0.5904 | $99.19 | | L4 | $0.7992 | $134.27 | | A10 | $1.1016 | $185.07 | | L40S | $1.9512 | $327.80 | | A100-40GB | $2.0988 | $352.60 | | A100-80GB | $2.4984 | $419.73 | | H100 | $3.9492 | $663.47 | These numbers are GPU-only. CPU, memory, regional multipliers, non-preemptible execution, and storage can add extra cost. With a $240 budget, one L40S can stay warm for about 123 hours, so a full 7-day L40S warm deployment is over budget. A10 fits the 7-day GPU-only budget, but the current Gemma 4 12B vLLM deployment is validated on L40S and should not be moved to A10 without a separate benchmark. Override with env vars before deploy: ```powershell $env:VC_LLM_MODEL="google/gemma-4-E4B-it" $env:VC_LLM_GPU="A10" $env:VC_TTS_BACKEND="kokoro" $env:VC_IMAGE_GPU="L40S" ```