跳到主要内容
AI教程

使用 SGLang 部署 OpenJev 并进行单 Token 分类

了解如何在 Modal 上部署 openjev-sglang,提交 Noul、Choice 和 Score 评估,检查服务健康状况,配置限制和身份验证,并将 API 连接到现有的 SGLang 后端。

使用 SGLang 部署 OpenJev 并进行单 Token 分类

什么是 openjev-sglang?

openjev-sglang 是一个使用 SGLang 上的 Qwen3.6-35B-A3B 实现 TypeSafe/Jev HTTP API 的服务器。它会将分类问题转换为高效的首个 token 概率读取,而不是生成冗长的响应。

默认部署会在每个 NVIDIA B200 GPU 上运行一个 SGLang 容器。SGLang 0.5.19 提供 Rust 前端、基数缓存以及可中断的预填充 CUDA 图。另有一个独立的 Python 进程使用 FastAPI、uvloop、基于 Rust 的 Hugging Face 分词器,以及与本地主机上 SGLang 的异步连接池来处理公共 API。

不会向 TypeSafe 发送任何请求。公共端点提供评估 API,而 SGLang 的生成和管理路由则继续仅在本地主机上保持私有。

主要功能

  • 三种评估类型: Noul 用于判断“是/否”概率,Choice 用于分类选择,Score 用于有序等级评分。
  • 单 token 推理: 每个问题都根据答案标签的对数概率进行评估,不生成思维链。
  • 共享前缀缓存: 在并发运行问题分支前,先预热公共提示词前缀。
  • 最多 64 个问题: Choice 和 Score 问题默认支持 2 到 64 个答案。
  • 结构化状态: 状态和指令可以是字符串、JSON 对象或数组。
  • 运维端点: 内置健康检查、存活检查、模型目录、限制信息、OpenAPI 架构、Swagger UI 以及交互式 Scalar 参考文档。
  • Modal 部署: 提供的应用支持自动扩缩容、持久化模型缓存、启动恢复以及缩容至零。
  • 可配置的保护机制: 支持请求限制、并发控制、超时以及可选的 Bearer 身份验证。

前置条件

安装 uv;如果计划使用提供的云端部署,还需获取 Modal 账户。首次构建会导入较大的 SGLang 镜像,首次启动 GPU 时会下载模型权重并编译或捕获内核。

CUDA 依赖保留在 SGLang 容器中。在工作站上运行 uv sync 只会安装 API、部署工具和测试。

安装项目

克隆仓库,进入其目录并同步 Python 环境:

uv sync

如果当前机器尚未完成 Modal 身份验证,请执行设置:

uv run modal setup

运行临时 Modal 部署

如需进行端到端测试,可启动临时服务器、运行包含的推理检查,并在完成后关闭服务器:

uv run modal run modal_app.py

此命令会将冒烟测试报告保存为 smoke-result.json。报告包含启动等待时间、推理延迟以及可用的缓存使用信息。

部署稳定端点

将应用部署为持久化的 Modal 服务:

uv run modal deploy modal_app.py

部署会输出类似 https://YOUR-SERVER.us-west.modal.direct 的 URL。将其存储在环境变量中,以便访问:

export OPENJEV_URL="https://YOUR-SERVER.us-west.modal.direct"

默认的 Modal 端点有意保持未进行身份验证。它使用美国西部路由区域,并可在配置的美国区域中调度计算资源。没有明确的容器上限,实例在空闲五分钟后会缩容至零。

已缩容至零的服务器在启动期间会返回 HTTP 503。包含的冒烟测试命令会自动重试这些启动响应。

针对已部署的 URL 运行冒烟测试:

uv run openjev smoke "$OPENJEV_URL"

冒烟测试会测试全部三种答案类型,检查包含 64 个答案的问题,执行基本语义检查,并验证 65 个答案会被拒绝。

提交首次评估

主要路由是 POST /v1/systemone。下面的请求使用 Noul、Choice 和 Score 问题对客户消息进行分类:

curl "$OPENJEV_URL/v1/systemone" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "jev-latest",
    "state": [
      {"role": "system", "content": "You are a support assistant."},
      {"role": "user", "content": "I was charged twice. Please refund the duplicate."}
    ],
    "questions": {
      "refund": {
        "type": "noul",
        "instructions": "Does the user request a refund?"
      },
      "department": {
        "type": "choice",
        "instructions": "Which department should handle this?",
        "criteria": {
          "billing": "Payments and refunds",
          "technical": "Software bugs"
        }
      },
      "urgency": {
        "type": "score",
        "instructions": "How urgent is the request?",
        "criteria": ["Routine", "Urgent", "Emergency"]
      }
    }
  }'

也可以将 JSON 保存在文件中,然后无需进行 shell 引号转义即可提交:

curl "$OPENJEV_URL/v1/systemone" \
  -H 'Content-Type: application/json' \
  --data-binary @examples/request.json

了解问题类型

  • Noul: 返回 yes 的归一化概率。
  • Choice: 返回概率最高的选项以及完整的概率分布。响应中会保留原始选项名称。
  • Score: 返回从零开始的期望等级,该值根据每个等级的索引及其概率计算,并同时返回图例。

选项会以字母标签及其描述的形式呈现给模型。响应键通常对模型隐藏。如果选项描述为 null,则使用其键来提供含义。

提供状态和指令

state 和 instructions 均接受字符串、JSON 对象或数组。聊天消息列表,或包含恰好为 {"messages": [...]} 的对象,会使用模型原生的聊天模板进行渲染。

原始消息的角色和对象会保留,每个分类问题都会作为另一条用户消息添加。其他结构化值会完整序列化到用户消息中。包含 messages 及其他字段的对象也会保留,以免元数据被静默丢弃。

聊天状态仅支持文本内容。不支持图像、音频和视频内容。

探索服务端点

  • POST /v1/systemone 执行 Noul、Choice 和 Score 评估。
  • GET /v1/models 返回包含 TypeSafe 和 OpenAI 风格字段的模型目录。
  • GET /v1/limits 报告接入限制。
  • GET /health 报告就绪状态、SGLang 健康状况和启动耗时。
  • GET /health/live 检查 API 进程是否存活。
  • GET / 打开 Scalar API 参考页面,其中包含可编辑的请求示例。
  • GET /docs 打开内置的 Swagger UI。
  • GET /openapi.json 返回生成的 OpenAPI 架构。

请求中的模型名称 jev-latest 是兼容性别名。默认的公开模型 ID 为 Qwen/Qwen3.6-35B-A3B,而 NVIDIA 的仓库在内部用作权重来源。

推理工作方式

  1. API 会校验架构、答案数量、请求体大小、上下文长度和总令牌预算。
  2. API 在禁用思考的情况下使用原生聊天模板渲染一次,然后将共享前缀与分别进行令牌化的问题后缀分离。
  3. API 将公共前缀以 max_new_tokens=1 发送到 SGLang。采样出的令牌会被丢弃,使该前缀可供机会式 radix 缓存复用。
  4. API 会为每个问题并发提交一个分支。每个分支包含前缀、问题后缀、助手标头和 Answer: 标记。
  5. API 仅请求有效答案标签的对数概率,并使用稳定的 softmax 对其重新归一化。

对于 N 个问题,服务器会发起 N + 1 次单令牌调用:一次缓存预热调用,以及每个问题各一次调用。生成的令牌会被忽略,因此不存在自回归续写或生成的思维链。未启用推测解码。

标签从 A 到 Z,随后使用经过验证的单令牌组合,例如 AA 和 AB。启动期间会使用活动令牌器检查全部 64 个标签。这样可以避免使用 10 或 64 之类的多令牌数字标签。

了解用量和缓存指标

响应标头 x-openjev-prefix-tokens 报告请求的公共前缀大小。当 SGLang 提供缓存计数时,x-openjev-cached-tokens 包含各分支缓存命中的总和。

SGLang 0.5.19 的 Rust 前端不会公开这些缓存计数。在该配置下,标头会缺失,冒烟测试报告会记录 null,而不是零。调度器日志仍可显示实际的缓存命中情况。Server-Timing 标头会分别记录提示词准备、共享预填充和分支推理的耗时。

usage.input_tokens 是预热调用和分支调用所报告的完整提示词数量之和,其中包括缓存令牌。usage.output_tokens 为 N + 1。这些数值描述的是后端活动,而不是唯一计算的令牌数或 TypeSafe 计费估算。

配置限制和身份验证

默认接入限制包括:64 个问题;每个 Choice 或 Score 问题 2 至 64 个答案;2 MiB JSON 请求体;每个分支(包括输出)32,768 个令牌;提交的输入令牌总数 262,144;16 个并发评估;以及 64 个并发后端调用。

无效请求会在推理前返回 HTTP 422。超过大小限制的请求体返回 413,过载返回带有 Retry-After 的 529,后端超时返回 504。失败或取消的评估会取消同级分支,并尝试在 SGLang 中中止这些分支。

可通过 OPENJEV_* 环境变量进行配置。常用设置包括:

  • OPENJEV_MODEL:默认为 nvidia/Qwen3.6-35B-A3B-NVFP4。
  • OPENJEV_SERVED_MODEL_NAME:覆盖公开模型 ID,并将其传递给 SGLang。
  • OPENJEV_REVISION:选择固定的检查点版本。
  • OPENJEV_FRONTEND:默认为 rust;python 是显式备用选项。
  • OPENJEV_MAX_INPUT_TOKENS:默认为 32768。
  • OPENJEV_MAX_TOTAL_INPUT_TOKENS:默认为 262144。
  • OPENJEV_MAX_CONCURRENT_REQUESTS:默认为 16。
  • OPENJEV_MAX_CONCURRENT_BRANCHES:默认为 64。
  • OPENJEV_REQUEST_TIMEOUT:默认为 120 秒。
  • OPENJEV_TEMPERATURE:默认为 1.0,会影响标签归一化。
  • OPENJEV_API_KEY:为公共 API 启用可选的 Bearer 身份验证。
  • OPENJEV_BACKEND_API_KEY:为 SGLang 配置单独的可选 Bearer 密钥。

Modal 启动器会自动从本地环境转发 OPENJEV_PROFILE、OPENJEV_FRONTEND 和 OPENJEV_SERVED_MODEL_NAME。将其他远程设置添加到 image.env(...),或使用 Modal Secret 存储凭据。

使用现有 SGLang 后端在本地运行

要使用已运行的后端,请将 API 指向其本地 URL:

uv run openjev serve --connect http://127.0.0.1:30000

后端必须使用匹配的模型和分词器版本,并支持所选 token 的对数概率、基数缓存以及足够的上下文长度。

如果 B200 主机或容器在独立的 Python 环境中安装了 SGLang 0.5.19,请显式提供其解释器:

uv run openjev serve --sglang-python /path/to/sglang/bin/python

开发与验证

使用随附的命令运行离线测试、分词器集成检查、代码检查和模式生成:

uv run pytest
uv run pytest -m integration
uv run ruff check .
uv run openjev schema

常规测试会离线覆盖单元行为和 API 行为。集成测试使用真实分词器,并会下载一个较小的 Hugging Face 文件,但不需要 GPU。模式生成同样不需要 GPU 或模型下载。

高级部署提示

保持 GPU 容器处于热状态

Modal 在服务空闲五分钟后会将其缩减至零。若持续较低的启动延迟比避免 GPU 空闲使用更重要,请在 modal_app.py 中设置 min_containers=1。

预留首次启动初始化时间

首次启动 GPU 时会下载权重并编译或捕获内核。模型权重、SGLang 调优数据和 Triton 编译数据会持久化在 openjev-huggingface Modal Volume 中。后续启动会复用这些文件,但启动时仍会执行 CUDA 图捕获。

将缓存复用视为机会性的优化

基数复用不是按请求固定的 KV 会话。混合 Qwen 模型中的递归状态、缓存页边界、缓存压力和并发流量都可能降低命中率。后端使用 --mamba-radix-cache-strategy extra_buffer。

谨慎解读置信度

OpenJev 将置信度定义为 1 - H(probabilities) / log(number_of_options),并将结果限制在零到一之间。均匀概率会产生零置信度,而集中于单一选项的概率分布会产生一。

这些概率以所提供的选项为条件,并可能受到提示词措辞和标签顺序的影响。它们不是经过校准的正确性估计。

为后端故障做好准备

如果 SGLang 意外退出,API 也会退出。随后,Modal 启动器会终止容器,以便 Modal 替换它,而不是让公共 HTTP 进程继续连接到已失效的推理后端。正常关闭时,两个监视器都会被正常禁用。

结语

openjev-sglang 为在 Qwen3.6 上运行兼容 Jev 的分类任务提供了一种专注的方式,并能高效读取单 token 概率。其 Modal 配置支持可扩展的 B200 部署,而本地命令则支持测试和连接现有 SGLang 服务。请从冒烟测试开始,验证限制和身份验证设置,并随着流量增长监控耗时和缓存标头。