使用 Modal 在云端 GPU 上运行 Outlines 结构化生成:从镜像构建到 JSON Schema 约束推理完整指南
【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines
导读
本文是一份端到端的实战指南,讲解如何借助 Modal 示例文件,可直接对照运行。
为什么用 Modal 跑 Outlines
Outlines 是一个结构化输出(Structured Outputs)生成框架,其核心思想是"在生成过程中保证输出结构合法,而不是生成完再靠解析去修补"。但要让这一能力落地,前提是你有一块能跑大模型的 GPU。Modal 是一个 Serverless 云平台,可以按需为你在云端创建 GPU 实例,并在几秒钟内完成代码打包、容器构建与调度。把两者结合,就能在没有本地怪兽级显卡的情况下,快速、弹性地在云端完成带格式约束的模型推理。
环境准备
官方建议在虚拟环境中安装modal与outlines。先创建并激活虚拟环境:
python -m venv venv source venv/bin/activate然后安装依赖包:
pip install modal outlines其中modal是 Modal 的 Python 客户端(用于定义镜像、App、函数并触发云端执行),outlines是本项目的结构化生成框架。如果你希望把仓库克隆到本地对照阅读,也可以先git clone本项目后再进入examples/目录查看配套示例。
构建容器镜像
Modal 通过Image对象声明运行环境。下面的代码创建了一个基于 Debian slim 的 Python 3.11 镜像,并预装 Outlines 及其依赖的 Transformers 生态组件:
from modal import Image, App, gpu import os # 创建 Modal App 对象,名称为 "outlines-app"。 # 还有其它可选参数,如 secrets、调度策略等。 app = App(name="outlines-app") # 指定要使用的语言模型。 # 另一个不错的选择是 "NousResearch/Hermes-2-Pro-Mistral-7B" language_model = "mistral-community/Mistral-7B-v0.2" # 请设置环境变量 HF_TOKEN,值为你的 Hugging Face API token。 # 下面代码中的 .env({...}) 部分会把本地环境中的 token 拷贝进容器。 outlines_image = Image.debian_slim(python_version="3.11").pip_install( "outlines", "transformers", "datasets", "accelerate", "sentencepiece", ).env({ # 这会把本地已有的 HF_TOKEN 环境变量带入容器。 'HF_TOKEN': os.environ['HF_TOKEN'] # 若想直接在代码中写死 token,取消下面一行注释并替换成你的 token。 # 'HF_TOKEN':'YOUR_TOKEN' })要点说明:
Image.debian_slim(python_version="3.11")声明基础镜像与 Python 版本;pip_install(...)中的transformers、datasets、accelerate、sentencepiece是 Outlines 通过from_transformers加载 Hugging Face 模型时所依赖的关键库。- 若目标模型是 Hugging Face 上的门控模型(gated model),必须在容器内提供访问 token。最佳实践是通过
.env({'HF_TOKEN': os.environ['HF_TOKEN']})从本地环境透传,避免把 token 明文写进代码;代码中也保留了"直接写死 token"的注释行作为备选方案。 - 仓库配套示例 examples/modal_example.py 对依赖版本做了显式锁定(如
outlines==1.0.0、transformers==4.38.2、datasets==2.18.0、accelerate==0.27.2),在生产环境建议同样固定版本以获得可复现的构建。
配置容器:启动时预热模型
对于需要长时间运行的 Modal 应用,官方建议在容器启动时下载模型,而不是等到函数被调用时才下载。这样模型权重会被缓存,后续冷启动与重复运行都会显著加速:
# 该函数负责从 Hugging Face 拉取模型。 # Modal 容器启动时会调用它,适合做模型下载、环境变量初始化等准备工作。 def import_model(): import outlines import transformers outlines.from_transformers( transformers.AutoModelForCausalLM.from_pretrained(language_model), transformers.AutoTokenizer.from_pretrained(language_model) ) # 这行代码告诉容器在启动时执行 import_model 函数。 outlines_image = outlines_image.run_function(import_model)run_function是 Modal 镜像构建阶段的能力:它会在构建镜像时于容器内执行一次import_model,把模型权重固化进镜像层,从而避免每次冷启动都重新下载数 GB 的参数文件。这也是后面推理函数内只需"加载权重到显存"而非"下载权重"的前提。
定义输出 Schema:约束 JSON 结构
我们将复现 README 中的 JSON 结构化生成示例,为角色描述定义一个 JSON Schema。Schema 中包含一个带name、age、armor、weapon、strength字段的角色对象,其中armor与weapon通过$ref引用枚举定义:
schema = """{ "title": "Character", "type": "object", "properties": { "name": { "title": "Name", "maxLength": 10, "type": "string" }, "age": { "title": "Age", "type": "integer" }, "armor": {"$ref": "#/definitions/Armor"}, "weapon": {"$ref": "#/definitions/Weapon"}, "strength": { "title": "Strength", "type": "integer" } }, "required": ["name", "age", "armor", "weapon", "strength"], "definitions": { "Armor": { "title": "Armor", "description": "An enumeration.", "enum": ["leather", "chainmail", "plate"], "type": "string" }, "Weapon": { "title": "Weapon", "description": "An enumeration.", "enum": ["sword", "axe", "mace", "spear", "bow", "crossbow"], "type": "string" } } }"""这个 schema 中的enum是约束的核心:模型只能在["leather", "chainmail", "plate"]中选择装甲,只能在六种武器中二选一或择一,age与strength必须输出整数,name不得超过 10 个字符。在推理时,Outlines 会把该 schema 编译为 logits 处理器(logits processor),在每一步解码时屏蔽掉不可能满足 schema 的 token,从而从机制上保证输出合法。
从源码看,Outlines 的JsonSchema类型位于 src/outlines/types/dsl.py,它的构造参数非常灵活:除了本文使用的 JSON schema字符串,还接受dict、Pydantic 模型、TypedDict、dataclass 以及 genSON schema builder。构造时会通过jsonschema.Draft7Validator.check_schema对 schema 做合法性校验,并支持whitespace_pattern与ensure_ascii等选项。在outlines.types包中,JsonSchema与CFG、Regex、Choice等 DSL 类型一起构成结构化输出的类型体系(见 src/outlines/types/init.py),并在顶层通过outlines.json_schema导出(见 src/outlines/init.py)。
编写云端推理函数
在 Modal 上做推理,需要把推理逻辑包进@app.function装饰器中,并把镜像与 GPU 规格作为参数传入。下面选择 A100 80GB 实例:
@app.function(image=outlines_image, gpu=gpu.A100(size='80GB')) def generate( prompt: str = "Amiri, a 53 year old warrior woman with a sword and leather armor.", ): # 注意:此函数运行在容器内,因此需要在这里导入所需库。 import outlines import transformers from outlines.types import JsonSchema # 将模型加载进显存。前面的 import_model 已完成下载, # 所以这里只负责把权重加载到 GPU 内存。 outlines.from_transformers( transformers.AutoModelForCausalLM.from_pretrained(language_model, device_map="cuda"), transformers.AutoTokenizer.from_pretrained(language_model) ) # 基于模型与 JSON schema 创建生成器。 generator = outlines.Generator(model, JsonSchema(schema)) # 用指令标签 ([INST] 与 [/INST]) 包裹 prompt,指示这是指令任务。 # 不同模型的指令标签格式不同,请以模型官方文档为准。 character = generator( f"<s>[INST]Give me a character description. Describe {prompt}.[/INST]" ) # 打印生成的角色描述。 print(character)几个值得展开的实现细节:
- 为什么要在函数内重新导入库:
@app.function中的函数体运行在远端容器进程里,与本地 Python 进程不共享命名空间,因此outlines、transformers需要在函数内显式导入。 device_map="cuda":把模型权重显式放到 GPU 上。仓库中from_transformers的实现位于 src/outlines/models/transformers.py,它根据传入的是PreTrainedTokenizer还是ProcessorMixin分别构造Transformers或TransformersMultiModal包装器。该包装器的generate方法会把用户传入的输出类型转成transformers的LogitsProcessorList后交给model.generate执行(见同文件 L269-L313)。outlines.Generator(model, JsonSchema(schema))做了什么:见 src/outlines/generator.py,SteerableGenerator在构造时会把JsonSchema类型通过python_types_to_terms归一化,再调用get_json_schema_logits_processor预编译出 logits 处理器并缓存。logits 处理器的构建通常比较昂贵,因此 Generator 把它存储复用,在每次调用时reset()后传入模型(见 L280-L300)。这正是"结构化生成"的底层原理:推理过程中每一步采样都被 logits 处理器约束,只允许产出符合 JSON Schema 的 token 序列。- 指令标签(instruction tags):Mistral 系模型要求用
<s>[INST] ... [/INST]包裹指令,不同厂商/家族的模型标签格式可能不同,务必查阅所选模型文档。
定义本地入口并触发云端执行
@app.local_entrypoint()装饰器把main声明为使用 Modal CLI 启动时的本地入口函数:
@app.local_entrypoint() def main( prompt: str = "Amiri, a 53 year old warrior woman with a sword and leather armor.", ): # 调用上面定义的 generate 函数。.remote() 表示在云端机器上运行; # 若想本地运行可改用 .local(),但需要额外的环境配置。 generate.remote(prompt)generate.remote(prompt)会触发一次远程函数调用:Modal 先在云端拉起(或复用)配置了outlines_image的容器,将请求调度到 A100 GPU 上执行generate,再把函数内的print输出回传终端。
仓库中的 examples/modal_example.py 是同一方案的完整可运行版本,可作为对照:它固定了依赖版本、使用mistralai/Mistral-7B-Instruct-v0.2模型、用gpu="A100-40GB"声明 GPU(字符串形式的简写),并采用outlines.json_schema(schema)的简洁 API 直接在模型调用中指定 schema。你可以把本指南中的代码保存为example.py,或直接参考该文件。
在云端运行
首先确认已安装 Modal 客户端(如未安装则执行):
pip install modal然后获取 Modal 访问 token 并完成本地配置:
modal setup按提示完成认证后,一条命令即可在云端运行推理:
modal run example.py运行过程中你会看到 Modal 应用初始化(构建镜像、拉起容器、加载模型),随后终端中很快出现print函数输出的角色描述,例如一个包含name、age、armor、weapon、strength五个字段、且完全符合上文 JSON Schema 的合法 JSON。至此,一次"云端 GPU + 结构化生成"的完整链路就跑通了。
常见问题与调优建议
- 门控模型 403:确保本地已设置
HF_TOKEN环境变量且 token 具备目标模型仓库的访问权限;镜像构建时.env(...)只在有该环境变量时才能成功透传。 - 冷启动过慢:确认
import_model已通过run_function挂到镜像上,让模型下载发生在镜像构建阶段而非运行时;首次运行后 Modal 会缓存镜像与模型,后续启动显著加快。 - GPU 选型:7B 级模型用 A100-40GB 即可(见 examples/modal_example.py),更大模型再考虑 A100-80GB 或更高规格;Modal 支持多种 GPU 简写,可参考其官方 GPU 文档。
- 调试与验证:本仓库的 tests/models/test_transformers.py 展示了
from_transformers的实例化契约(如错误传入int会抛ValueError、Mamba/BART 等架构均被支持、device_dtype参数可指定torch.bfloat16等),本地调试结构化生成时可参考这些测试来验证模型包装是否正确。
小结
本文完整演示了"Modal 云端 GPU + Outlines 结构化生成"的落地路径:先通过Image.debian_slim().pip_install().env().run_function()构建并预热镜像,再用@app.function(image=..., gpu=...)声明带 GPU 的推理函数,配合outlines.Generator(model, JsonSchema(schema))完成受 JSON Schema 约束的生成,最后由@app.local_entrypoint()加modal run一键触发。整个过程无需本地 GPU,也无需手动运维云主机——这正是 Serverless 平台与结构化生成框架结合的价值所在。
【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考