mcpField
S
serverless-modal

serverless-modal

github.com/wanshuiyin云与 DevOps18.2k470 次安装4 天前更新
代码仓库

关于 serverless-modal

serverless-modal 是用于通过 Python 脚本在 Modal 上运行 GPU 训练、评估和推理任务的技能。它提供一次性任务、API、批处理及分布式训练启动模式,并要求每次运行前估算成本。

主要功能

  1. 01生成 Modal 启动脚本
  2. 02支持训练、推理与评估
  3. 03提供批处理和多 GPU 模式
  4. 04运行前估算成本
  5. 05从卷或函数输出收集结果
  6. 06监控应用和日志

适用场景

  1. 01无需管理 SSH 实例即可运行 GPU 实验
  2. 02部署持久运行的推理 API
  3. 03通过并行 GPU 函数处理数据集
  4. 04进行微调或多 GPU 训练

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wanshuiyin/auto-claude-code-research-in-sleep --skill serverless-modal

安装后在 Claude Code 中输入/serverless-modal确认已加载。

详细介绍

Modal Cloud GPU — 训练与推理

任务:$ARGUMENTS

概述

Modal 是一个无服务器 GPU 云平台。相比基于 SSH 的平台(vast.ai、远程服务),它的主要优势包括:

  • 零配置:无需 SSH、Docker 或端口转发。编写 Python → modal run → 完成。
  • 自动缩容至零:代码运行结束的瞬间即停止计费。没有闲置实例。
  • 本地优先:在笔记本电脑上运行 modal run。代码、数据和结果都保留在本地;只有 GPU 函数在远程运行。
  • 环境可复现:依赖项通过代码中的 modal.Image 声明,而不是系统级软件包。 将 modal.Image 链视为 ../shared-references/compute-env-contract.md 中声明式环境规范的渲染形式—— 规范字段相同(基础镜像、按顺序排列的 pip 阶段、 环境变量、烟雾测试探针),.aris/compute/modal.md 中的 env:<name>@<specHash> 账本条目也相同, 长时间运行前的三级验证也相同。

适用场景:没有本地 GPU、需要调试 CUDA 代码、运行小规模测试或快速迭代实验的用户。$5 免费额度(无需银行卡)足以调试代码;$30 免费额度(需绑定银行卡)足以完成大多数小规模实验运行。

权衡:Modal 的每 GPU 小时费用高于某些 GPU 档位下的 vast.ai 或 Lightning,但无需花时间配置,也不会为闲置资源付费,因此短期或中期工作负载通常更便宜。对于长时间训练(>4 小时),可考虑使用 vast.ai,以降低每小时费用。

身份验证

pip install modal
modal setup          # Opens browser login, writes token to ~/.modal.toml
# Verify:
modal run -q 'print("ok")'
  • 注册:https://modal.com(使用 GitHub/Google 登录)
  • 免费(无需银行卡):$5/月——足以进行快速测试
  • 免费(需绑定银行卡):$30/月——在 https://modal.com/settings 绑定付款方式,即可享受完整免费额度。设置工作区消费限额,避免意外超额扣费(Settings → Usage → Spending Limit)
  • 学术机构:申请 $10k 额度 | 初创公司:申请 $25k 额度
  • 密钥:modal secret create huggingface-secret HF_TOKEN=hf_xxxxx

推荐设置:绑定银行卡以解锁每月 $30 额度,然后立即设置消费限额(例如 $30),确保不会超出免费额度。达到限额时,Modal 会暂停你的工作负载。

安全警告:务必直接在浏览器中访问 https://modal.com/settings,绑定银行卡并设置消费限额。切勿通过 Claude Code 或任何命令行工具输入付款信息、银行卡号或账单详情。只有 Modal 官方网站可安全地用于付款操作。

定价(来源:modal.com/pricing,按秒计费)

GPU$/秒≈$/小时显存带宽 GB/s免费额度 → 时长
T4$0.000164$0.5916GB300~8.5 小时($5)/ 50.8 小时($30)
L4$0.000222$0.8024GB300~6.3 小时 / 37.5 小时
A10$0.000306$1.1024GB600~4.5 小时 / 27.3 小时
L40S$0.000542$1.9548GB864~2.6 小时 / 15.4 小时
A100-40GB$0.000583$2.1040GB1555~2.4 小时 / 14.3 小时
A100-80GB$0.000694$2.5080GB2039~2.0 小时 / 12.0 小时
H100$0.001097$3.9580GB3352~1.3 小时 / 7.6 小时
H200$0.001261$4.54141GB4800~1.1 小时 / 6.6 小时
B200$0.001736$6.25192GB8000~0.8 小时 / 4.8 小时

CPU:$0.047/核心/小时 | RAM:$0.008/GiB/小时(GPU 通常占总成本的 90% 以上)

!! 必须估算成本 !!

每次运行前,必须估算成本并向用户展示,获得确认后再运行。

要点:

  • 推理瓶颈在于内存带宽,而非计算能力 → 高带宽 GPU 通常总体成本更低
  • 7-8B BF16 推理需要约 22GB 显存(权重 15G + KV 缓存 1G + 开销),T4(16GB)显存不足
  • 用于基准测试时,H100 通常比 L4 更便宜(速度快 11 倍,但只贵 5 倍)
成本估算模板(每次运行前必须使用)
Cost estimate (Modal):
  Model: [name] ([params], [precision])
  VRAM: ~[X]GB (weights + KV cache + overhead)
  GPU: [type] ([VRAM]GB, $[X]/sec = $[X]/hr, bandwidth [X] GB/s)
  Estimate: ~[N] min, ~$[X]
7-8B BF16 基准测试成本对比
GPU速度 tok/s$/小时1000 个样本 x 200tok 的成本时长
H100224$3.95$0.9815 分钟
A100-40GB104$2.10$1.1232 分钟
L420$0.80$2.22167 分钟

工作流

步骤 1:分析任务 → 估算成本 → 选择 GPU

与任何 GPU 技能的分析方式相同——根据模型大小确定显存需求,选择 GPU,估算时长并计算成本。请参阅上方的定价表。

显存经验法则:

模型大小FP16 显存推荐 GPU
≤3B~8GBT4、L4
7-8B~22GBL4、A10、A100-40GB
13B~30GBL40S、A100-40GB
30B~65GBA100-80GB、H100
70B~140GBH100:2、H200
步骤 2:生成 Modal 启动器

根据任务类型生成合适的启动脚本。

模式 A:一次性 GPU 函数(训练、评估、基准测试)

这是 run-experiment 集成中最常见的模式。将现有训练脚本封装起来:

import modal

app = modal.App("experiment-name")
# One .pip_install() call per SPEC PHASE (chained calls install in order, so a
# pinned torch in the first call can't be dragged by packages in the second —
# the rendered form of compute-env-contract.md's ordered pip_phases):
image = (
    modal.Image.debian_slim(python_version="3.11")
    .pip_install("torch")                                        # phase 1: pins
    .pip_install("transformers", "accelerate", "datasets", "wandb")  # phase 2
)

# Mount local project code into the container
local_code = modal.Mount.from_local_dir(".", remote_path="/workspace")
# Persistent volume for checkpoints and results
volume = modal.Volume.from_name("experiment-results", create_if_missing=True)

@app.function(
    image=image,
    gpu="A100-80GB",          # Chosen based on Step 1 analysis
    mounts=[local_code],
    volumes={"/results": volume},
    timeout=3600 * 6,         # 6 hours max
    secrets=[modal.Secret.from_name("wandb-secret")],  # Optional
)
def train():
    import subprocess
    subprocess.run(
        ["python", "train.py", "--output_dir", "/results/run_001"],
        cwd="/workspace",
        check=True,
    )
    volume.commit()  # Persist results to volume

@app.local_entrypoint()
def main():
    train.remote()
    print("Training complete. Results saved to Modal volume 'experiment-results'.")

运行:modal run launcher.py

模式 B:Web API(持久推理服务)
import modal

app = modal.App("inference-api")
image = (
    modal.Image.debian_slim(python_version="3.11")
    .pip_install("torch")                          # phase 1: pins
    .pip_install("transformers", "accelerate")     # phase 2
)

@app.cls(image=image, gpu="L40S")
@modal.concurrent(max_inputs=10)
class InferenceAPI:
    @modal.enter()
    def load_model(self):
        from transformers import AutoModelForCausalLM, AutoTokenizer
        self.tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B")
        self.model = AutoModelForCausalLM.from_pretrained(
            "meta-llama/Llama-3.2-1B", device_map="auto"
        )

    @modal.fastapi_endpoint(method="POST")
    def generate(self, request: dict):
        inputs = self.tokenizer(request.get("prompt", ""), return_tensors="pt").to("cuda")
        outputs = self.model.generate(**inputs, max_new_tokens=256)
        return {"text": self.tokenizer.decode(outputs[0], skip_special_tokens=True)}

部署:modal deploy app.py

模式 C:vLLM 高性能推理
import modal, subprocess

app = modal.App("vllm-server")
image = modal.Image.debian_slim(python_version="3.11").pip_install("vllm")
VOLUME = modal.Volume.from_name("model-cache", create_if_missing=True)
MODEL = "Qwen/Qwen3-4B"

@app.function(image=image, gpu="H100", volumes={"/models": VOLUME}, timeout=3600)
@modal.concurrent(max_inputs=100)
@modal.web_server(port=8000)
def serve():
    subprocess.Popen(["python", "-m", "vllm.entrypoints.openai.api_server",
                      "--model", MODEL, "--download-dir", "/models", "--port", "8000"])
模式 D:批量并行(遍历数据集)
@app.function(image=image, gpu="T4", timeout=600)
def process_item(item: dict) -> dict:
    # ... process one item ...
    return {"result": "processed"}

@app.local_entrypoint()
def main():
    results = list(process_item.map([{"id": i} for i in range(1000)]))
模式 E:LoRA 微调
@app.function(
    image=image, gpu="A100-80GB", volumes={"/output": volume},
    timeout=3600 * 6, secrets=[modal.Secret.from_name("huggingface-secret")],
)
def train():
    # ... transformers + peft + trl training code ...
    trainer.save_model("/output/final")
    volume.commit()
模式 F:多 GPU 分布式训练
@app.function(image=image, gpu="H100:4", volumes={"/output": volume}, timeout=3600 * 12)
def train_distributed():
    import subprocess
    subprocess.run(["accelerate", "launch", "--num_processes", "4",
                    "--mixed_precision", "bf16", "train.py"], check=True)
步骤 3:运行
modal run launcher.py     # One-shot execution (most common for experiments)
modal deploy app.py       # Persistent service deployment
步骤 4:验证并监控
modal app list            # List running apps
modal app logs <app-name> # Stream logs
步骤 5:收集结果

结果收集方式取决于所用模式:

基于 Volume(推荐用于训练):

# Download results from volume after run completes
# Option A: In the launcher script, copy results to local mount before exit
# Option B: Use modal volume commands
modal volume ls experiment-results
modal volume get experiment-results /run_001/results.json ./results/

基于标准输出/返回值(用于评估/基准测试): 结果会打印到终端或由函数返回——已经位于本地。

步骤 6:清理

Modal 会自动缩容至零——无需手动销毁实例。但请清理未使用的资源:

modal app stop <app-name>     # Stop a deployed service
modal volume rm <volume-name> # Delete a volume when done

CLI 参考

modal run app.py          # Run once
modal deploy app.py       # Deploy persistent service
modal app logs <app>      # View logs
modal app list            # List apps
modal app stop <app>      # Stop
modal volume ls           # List volumes
modal volume get <vol> <remote> <local>  # Download from volume
modal secret create NAME KEY=VALUE       # Create secret

关键提示

  • GPU 回退:gpu=["H100", "A100-80GB", "L40S"] — Modal 会按顺序尝试
  • 多 GPU:gpu="H100:4"(最多 8 个 GPU,成本线性增长)
  • Volume:使用 modal.Volume.from_name("x", create_if_missing=True) 进行持久化存储
  • @modal.enter() 每个容器只加载一次模型 | 使用 @modal.concurrent() 处理并发请求
  • 长时间训练:设置 timeout=3600 * N(默认值为 5 分钟)
  • 本地代码:modal.Mount.from_local_dir(".", remote_path="/workspace")
  • W&B 集成:secrets=[modal.Secret.from_name("wandb-secret")] + 在脚本中调用 wandb.init()

与其他技能组合

/run-experiment "train model"       <- detects gpu: modal, calls /serverless-modal
  -> /serverless-modal              <- analyzes task, generates launcher, runs
  -> Results returned locally or to Modal Volume
  -> No destroy step needed (auto scale-to-zero)

/serverless-modal                   <- standalone: any Modal GPU workload
/serverless-modal "deploy vLLM"     <- inference service deployment

CLAUDE.md 示例

## Modal
- gpu: modal                 # tells run-experiment to use Modal serverless
- modal_gpu: A100-80GB       # optional: override GPU selection (default: auto-select)
- modal_timeout: 21600       # optional: max seconds (default: 6 hours)
- modal_volume: my-results   # optional: named volume for results persistence

无需 SSH 密钥、Docker 镜像或实例管理。只需执行 pip install modal && modal setup。

成本保护:完成 modal setup 后,在浏览器中访问 https://modal.com/settings(切勿通过 CLI 操作)→ 绑定付款方式以解锁每月 $30 的免费额度(不绑定银行卡:每月仅 $5)。然后将工作区支出上限设为等同于免费额度的金额——达到上限时,Modal 会自动暂停工作负载,避免产生意外费用。

文档

评论

支持 Markdown