From 087ac38d6d7123e9910f7bfb37a447ce1651405a Mon Sep 17 00:00:00 2001 From: zuiho <2324465096@qq.com> Date: Fri, 31 Jul 2026 11:35:20 +0800 Subject: [PATCH] Add deterministic vLLM-Omni release audit --- .github/workflows/vllm-omni-release-audit.yml | 99 +++ README.md | 1 + adapters/vllm_omni/release_baseline.yaml | 144 ++++ doc/KNOWLEDGE.md | 3 +- doc/VLLM_OMNI_RELEASE_MAINTENANCE.md | 51 ++ .../repos/vllm-omni/ci/guides/test-tiers.md | 8 +- .../components/configuration/_index.md | 2 + .../vllm-omni/components/diffusion/_index.md | 2 +- .../components/distributed/_index.md | 2 +- .../components/model-executor/_index.md | 5 +- .../vllm-omni/components/scheduler/_index.md | 2 +- .../vllm-omni/components/serving/_index.md | 2 +- knowledge/repos/vllm-omni/models/catalog.md | 38 +- .../vllm-omni/models/voxtral-tts/_index.md | 13 +- test/test_release_audit.py | 355 +++++++++ tools/audit_vllm_omni_release.py | 17 + tools/vllm_omni_release_audit.py | 751 ++++++++++++++++++ 17 files changed, 1461 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/vllm-omni-release-audit.yml create mode 100644 adapters/vllm_omni/release_baseline.yaml create mode 100644 doc/VLLM_OMNI_RELEASE_MAINTENANCE.md create mode 100644 test/test_release_audit.py create mode 100644 tools/audit_vllm_omni_release.py create mode 100644 tools/vllm_omni_release_audit.py diff --git a/.github/workflows/vllm-omni-release-audit.yml b/.github/workflows/vllm-omni-release-audit.yml new file mode 100644 index 0000000..7437e90 --- /dev/null +++ b/.github/workflows/vllm-omni-release-audit.yml @@ -0,0 +1,99 @@ +name: vLLM-Omni release drift audit + +on: + pull_request: + paths: + - .github/workflows/vllm-omni-release-audit.yml + - adapters/vllm_omni/manifest.yaml + - adapters/vllm_omni/release_baseline.yaml + - doc/VLLM_OMNI_RELEASE_MAINTENANCE.md + - knowledge/repos/vllm-omni/** + - tools/audit_vllm_omni_release.py + - tools/vllm_omni_release_audit.py + schedule: + - cron: "23 3 * * 1" + workflow_dispatch: + inputs: + from_ref: + description: Old tag or SHA; blank uses the audited baseline + required: false + type: string + to_ref: + description: New tag or SHA + required: true + default: origin/main + type: string + mode: + description: Scheduled checks use report-only + required: true + default: report-only + type: choice + options: + - report-only + - enforce + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check out vLLM-Omni + uses: actions/checkout@v4 + with: + repository: vllm-project/vllm-omni + path: .audit/vllm-omni + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install audit dependency + run: python -m pip install PyYAML + + - name: Resolve audit inputs + id: inputs + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + REQUESTED_FROM: ${{ inputs.from_ref }} + REQUESTED_TO: ${{ inputs.to_ref }} + REQUESTED_MODE: ${{ inputs.mode }} + run: | + read_baseline() { + python -c "import yaml; print(yaml.safe_load(open('adapters/vllm_omni/release_baseline.yaml'))['upstream']['$1'])" + } + if [[ "$EVENT_NAME" == "pull_request" ]]; then + echo "from=$(read_baseline previous_audited_sha)" >> "$GITHUB_OUTPUT" + echo "to=$(read_baseline audited_sha)" >> "$GITHUB_OUTPUT" + echo "mode=enforce" >> "$GITHUB_OUTPUT" + else + audited_sha="$(read_baseline audited_sha)" + echo "from=${REQUESTED_FROM:-$audited_sha}" >> "$GITHUB_OUTPUT" + echo "to=${REQUESTED_TO:-origin/main}" >> "$GITHUB_OUTPUT" + echo "mode=${REQUESTED_MODE:-report-only}" >> "$GITHUB_OUTPUT" + fi + + - name: Run release audit + run: | + python tools/audit_vllm_omni_release.py \ + --from "${{ steps.inputs.outputs.from }}" \ + --to "${{ steps.inputs.outputs.to }}" \ + --repo .audit/vllm-omni \ + --mode "${{ steps.inputs.outputs.mode }}" \ + --json-output "${RUNNER_TEMP}/vllm-omni-release-audit.json" + + - name: Upload machine report + if: always() + uses: actions/upload-artifact@v4 + with: + name: vllm-omni-release-audit + path: ${{ runner.temp }}/vllm-omni-release-audit.json + if-no-files-found: warn diff --git a/README.md b/README.md index ebd0a38..9960a14 100644 --- a/README.md +++ b/README.md @@ -123,5 +123,6 @@ Strict 模式需要用户明确提出。具体用法见 - [安装和 MCP 配置](doc/MCP.md) - [知识库贡献规范](knowledge/CONTRIBUTING.md) +- [vLLM-Omni 发版漂移审计](doc/VLLM_OMNI_RELEASE_MAINTENANCE.md) - [项目设计与实现](doc/) - [评测说明](eval/README.md) diff --git a/adapters/vllm_omni/release_baseline.yaml b/adapters/vllm_omni/release_baseline.yaml new file mode 100644 index 0000000..01267ee --- /dev/null +++ b/adapters/vllm_omni/release_baseline.yaml @@ -0,0 +1,144 @@ +schema_version: 1 + +upstream: + repository: vllm-project/vllm-omni + previous_audited_sha: 5d44868e918ecf9d3a6f1158c45acfad5989e1a7 + audited_ref: v0.26.0rc1 + audited_sha: 807db6efd70ff2e9b55a63d6e1b0530e2b74f8f2 + +# These fingerprints are generated from sorted registry/deploy inventories. +# They make the baseline exact without duplicating hundreds of entries here. +inventories: + autoregressive: + count: 72 + sha256: 79afac742125003b8d65a02e0e6b53398c4fc5f1c6e765fa8e1eb31f74871195 + diffusion: + count: 61 + sha256: f5c0b782cb98f20a9c7c77b696fda620a12d0899fd9e8abc9ccb696fe0c457e1 + pipelines: + count: 46 + sha256: ce4f6edb8748e030f04bf7a6fe038231453cd1cd75f64311c898acff8c14b14b + deploy_yamls: + count: 71 + sha256: fb4c591580262a2dc8d03dd8c7fd681d98486f31007696d56adf349c24c25147 + +# Structural review owners for changed upstream paths. This is deliberately +# separate from manifest.modules: changing that runtime map alters rebase fan-out. +path_owners: + benchmark: + - benchmarks/ + - vllm_omni/benchmarks/ + ci: + - .buildkite/ + - .github/ + - docker/ + configuration: + - vllm_omni/config/ + - vllm_omni/deploy/ + diffusion: + - vllm_omni/diffusion/ + distributed: + - vllm_omni/distributed/ + documentation: + - .claude/skills/ + - docs/ + - examples/ + - recipes/ + - realtime_video_prompt_interaction_protocol.md + model-executor: + - vllm_omni/attention/ + - vllm_omni/experimental/ + - vllm_omni/inputs/ + - vllm_omni/lora/ + - vllm_omni/model_executor/ + - vllm_omni/model_extras/ + - vllm_omni/outputs/ + - vllm_omni/platforms/ + - vllm_omni/plugins/ + - vllm_omni/quantization/ + - vllm_omni/reasoning/ + - vllm_omni/sample/ + - vllm_omni/tokenizers/ + - vllm_omni/transformers_utils/ + - vllm_omni/utils/ + - vllm_omni/worker/ + observability: + - vllm_omni/metrics/ + - vllm_omni/profiler/ + scheduler: + - vllm_omni/core/ + serving: + - vllm_omni/engine/ + - vllm_omni/entrypoints/ + testing: + - tests/ + tooling: + - tools/ + runtime-core: + - vllm_omni/__init__.py + - vllm_omni/data_entry_keys.py + - vllm_omni/request.py + +# Direct review destination for every structural owner above. The audit checks +# that each route has a real current knowledge entrypoint. +owner_documents: + benchmark: + - knowledge/repos/vllm-omni/benchmark/_index.md + ci: + - knowledge/repos/vllm-omni/ci/_index.md + configuration: + - knowledge/repos/vllm-omni/components/configuration/rules.md + diffusion: + - knowledge/repos/vllm-omni/components/diffusion/rules.md + distributed: + - knowledge/repos/vllm-omni/components/distributed/_index.md + documentation: + - knowledge/general/docs/_index.md + model-executor: + - knowledge/repos/vllm-omni/components/model-executor/rules.md + observability: + - knowledge/repos/vllm-omni/rules.md + scheduler: + - knowledge/repos/vllm-omni/components/scheduler/rules.md + serving: + - knowledge/repos/vllm-omni/components/serving/rules.md + testing: + - knowledge/repos/vllm-omni/ci/_index.md + tooling: + - knowledge/repos/vllm-omni/rules.md + runtime-core: + - knowledge/repos/vllm-omni/components/model-executor/rules.md + +ignored_paths: +- pattern: .gitignore + reason: repository metadata +- pattern: .pre-commit-config.yaml + reason: repository tooling +- pattern: .readthedocs.yaml + reason: documentation tooling +- pattern: CODE_OF_CONDUCT.md + reason: community metadata +- pattern: CONTRIBUTING.md + reason: community metadata +- pattern: LICENSE + reason: legal metadata +- pattern: README.md + reason: repository overview +- pattern: SECURITY.md + reason: community metadata +- pattern: setup.py + reason: package metadata +- pattern: pyproject.toml + reason: package metadata +- pattern: requirements/ + reason: package dependencies + +pin_documents: +- doc/KNOWLEDGE.md +- knowledge/repos/vllm-omni/models/catalog.md +- knowledge/repos/vllm-omni/components/configuration/_index.md +- knowledge/repos/vllm-omni/components/diffusion/_index.md +- knowledge/repos/vllm-omni/components/distributed/_index.md +- knowledge/repos/vllm-omni/components/model-executor/_index.md +- knowledge/repos/vllm-omni/components/scheduler/_index.md +- knowledge/repos/vllm-omni/components/serving/_index.md diff --git a/doc/KNOWLEDGE.md b/doc/KNOWLEDGE.md index 6649078..3c6bb3b 100644 --- a/doc/KNOWLEDGE.md +++ b/doc/KNOWLEDGE.md @@ -42,7 +42,8 @@ extensions merely because they are absent from the common source. history was removed — used for page frontmatter). - **Code-mirror pin:** the `knowledge/repos/vllm-omni/components/` source maps are verified against vllm-omni `main @ - 238fc0a609311235a671940cf209a7eb72c1dc29` (every stated path exists there). + 807db6efd70ff2e9b55a63d6e1b0530e2b74f8f2`. The canonical machine baseline + is `adapters/vllm_omni/release_baseline.yaml`. ## Layout: general vs repo-specific The knowledge base separates the two, and so do we — the whole tree is **shared**, diff --git a/doc/VLLM_OMNI_RELEASE_MAINTENANCE.md b/doc/VLLM_OMNI_RELEASE_MAINTENANCE.md new file mode 100644 index 0000000..a4b4070 --- /dev/null +++ b/doc/VLLM_OMNI_RELEASE_MAINTENANCE.md @@ -0,0 +1,51 @@ +# vLLM-Omni 发版漂移审计 + +InferMatrixCopilot 的模型清单、代码 owner 路由和源码引用会随着 +vLLM-Omni 发版而过期。这个审计只读取两个 Git 提交和本仓库声明,不会 checkout +目标版本,也不会自动修改知识规则。 + +## 本地运行 + +先在 vLLM-Omni checkout 中 fetch 需要比较的 tag 或 SHA,然后运行: + +```powershell +python tools/audit_vllm_omni_release.py ` + --from 5d44868e ` + --to v0.26.0rc1 ` + --repo D:\path\to\vllm-omni ` + --json-output $env:TEMP\vllm-omni-release-audit.json +``` + +审计覆盖: + +- AR、Diffusion 和 pipeline registry; +- deploy YAML; +- 新增、修改、删除和重命名路径; +- changed path 到 Direct 知识 owner 的机器路由; +- adapter runtime module 未覆盖的 changed path; +- active knowledge 中受删除或重命名影响的 `sources:`; +- component、model catalog 与 canonical baseline 的 source pin。 + +同样的提交和 baseline 会产生等价 JSON;报告不包含时间戳和本机 checkout 路径。 +默认 `--mode enforce`:存在未解释漂移时退出 1,输入或 Git 失败时退出 2。 +`--mode report-only` 仍报告 `DRIFT`,但退出 0,供定时巡检使用。 + +## 更新一个 release + +1. 用当前 `release_baseline.yaml` 的 audited SHA 作为 `--from`,新 tag 作为 + `--to`,先跑 `report-only`。 +2. 人工确认 registry、deploy、路径 owner 和知识来源变化。 +3. 把旧的 `audited_sha` 移到 `previous_audited_sha`,再只更新报告证明已经漂移的 + baseline、catalog、source map 或 manifest;不要自动生成语义规则。 +4. 用同一组 `--from/--to` 跑 `enforce`,再运行知识 validator 和相关 pytest。 +5. JSON 是临时证据,不提交到 `knowledge/`;完成后删除。 + +## PR 学习与 release 审计是两件事 + +合并 PR 的复盘只提炼可复用、可执行的 owner 规则,且必须等最终修复、review thread +和 CI 结果稳定后再做。Release 审计只对账结构事实,不把 PR 内容、事故过程或审计报告 +写进知识树,也不因为上游发版自动升级 InferMatrixCopilot 版本或依赖。 + +CI 每周对 upstream `main` 运行 `report-only`。修改 baseline 的 PR 会自动用 +`previous_audited_sha → audited_sha` 运行 `enforce`;Actions 手动运行可用 +`report-only` 检查任意区间,`enforce` 只接受当前 baseline 声明的升级区间。 diff --git a/knowledge/repos/vllm-omni/ci/guides/test-tiers.md b/knowledge/repos/vllm-omni/ci/guides/test-tiers.md index 15f8977..19476da 100644 --- a/knowledge/repos/vllm-omni/ci/guides/test-tiers.md +++ b/knowledge/repos/vllm-omni/ci/guides/test-tiers.md @@ -1,16 +1,16 @@ --- title: "测试分级(L1–L5)与 pytest markers" created: 2026-07-16 -updated: 2026-07-16 +updated: 2026-07-31 type: guide tags: [vllm-omni, ci] -sources: [docs/contributing/ci/CI_5levels.md, docs/contributing/ci/tests_markers.md] +sources: [docs/contributing/ci/test_system_overview.md, docs/contributing/ci/test_writing_guide.md] --- # 测试分级(L1–L5)与 pytest markers -官方 spec:`docs/contributing/ci/CI_5levels.md` + `tests_markers.md` -(`main @ 5c390096` 复核)。测试金字塔五级 + Common 规范 +官方 spec:`docs/contributing/ci/test_system_overview.md` + +`test_writing_guide.md`(`main @ 807db6ef` 复核)。测试金字塔五级 + Common 规范 (PR 模板/checklist 与 CI 失败说明)。 ## 五级定义 diff --git a/knowledge/repos/vllm-omni/components/configuration/_index.md b/knowledge/repos/vllm-omni/components/configuration/_index.md index e95ad1f..f9c8188 100644 --- a/knowledge/repos/vllm-omni/components/configuration/_index.md +++ b/knowledge/repos/vllm-omni/components/configuration/_index.md @@ -14,6 +14,8 @@ sources: ["claude-workflow-starter-private@296ea45", vllm_omni/config/] - 主要测试:`tests/config/`、`tests/test_config_factory.py`、`tests/test_diffusion_config_propagation.py`,以及各公开入口附近的配置测试 - 部署配置:`vllm_omni/deploy/*.yaml`,以及 `pipeline_registry.py`、 `endpoint_policy.py`、`server_settings.py`、`yaml_util.py`、`composable_parallel/` +- 源码校验:以上路径在 `main @ 807db6ef` 验证存在;机器基线见 + `adapters/vllm_omni/release_baseline.yaml` ## 什么时候查这里 diff --git a/knowledge/repos/vllm-omni/components/diffusion/_index.md b/knowledge/repos/vllm-omni/components/diffusion/_index.md index 63fb837..2bd5b05 100644 --- a/knowledge/repos/vllm-omni/components/diffusion/_index.md +++ b/knowledge/repos/vllm-omni/components/diffusion/_index.md @@ -10,7 +10,7 @@ sources: [] # Diffusion - 源码入口:`vllm_omni/diffusion/` 全树,含 16 个子模块:attention、cache、distributed、executor、hooks、layers、lora、model_loader、models、offloader、postprocess、profiler、quantization、sched、utils、worker -- 源码校验:以上子模块均已在 `main @ 238fc0a6`(此前亦在 `dev/vllm-align @ 4f2b32c` 验证,结果一致) 验证存在 +- 源码校验:以上子模块均已在 `main @ 807db6ef` 验证存在 - 主要职责:多个 diffusion 模型共用的 pipeline、执行循环、scheduler 接入和运行机制 ## 什么时候查这里 diff --git a/knowledge/repos/vllm-omni/components/distributed/_index.md b/knowledge/repos/vllm-omni/components/distributed/_index.md index 77e1ab4..0df0f56 100644 --- a/knowledge/repos/vllm-omni/components/distributed/_index.md +++ b/knowledge/repos/vllm-omni/components/distributed/_index.md @@ -14,7 +14,7 @@ sources: [vllm_omni/distributed/omni_connectors/, vllm_omni/distributed/omni_coo `vllm_omni/distributed/omni_coordinator/`(协调器与 load balancer) - 知识面另覆盖跨 stage ZMQ 路由/端口分配(`vllm_omni/engine/stage_engine_startup.py::OmniMasterServer`) ——组件划分服务知识归属,与 manifest 运行时粒度不同 -- 源码校验:以上路径与下列锚点均已在 `main @ 5c390096` 验证存在: +- 源码校验:以上路径与下列锚点均已在 `main @ 807db6ef` 验证存在: `OmniConnectorBase`(connectors/base.py:12)、`OmniKVTransferManager` (kv_transfer_manager.py:341)、`LoadBalancer` 三实现(load_balancer.py:39/64/74/102)、 `OmniMasterServer._allocate_route_locked`(stage_engine_startup.py:254) diff --git a/knowledge/repos/vllm-omni/components/model-executor/_index.md b/knowledge/repos/vllm-omni/components/model-executor/_index.md index 46b85aa..947529b 100644 --- a/knowledge/repos/vllm-omni/components/model-executor/_index.md +++ b/knowledge/repos/vllm-omni/components/model-executor/_index.md @@ -9,8 +9,9 @@ sources: [] # Model Executor -- 源码入口:`vllm_omni/model_executor/`(layers、model_loader、models、stage_configs、stage_input_processors)、`vllm_omni/worker/`(gpu_*_worker、gpu_*_model_runner、mixins)、`vllm_omni/inputs/`(runner 输入预处理:data.py、preprocess.py)和设备平台层 `vllm_omni/platforms//platform.py` -- 源码校验:以上路径均已在 `main @ 238fc0a6`(此前亦在 `dev/vllm-align @ 4f2b32c` 验证,结果一致) 验证存在;旧的 `platforms/*/worker/` 布局在该提交已不存在(平台目录只含 `platform.py`) +- 源码入口:`vllm_omni/model_executor/`(layers、model_loader、models、stage_input_processors)、`vllm_omni/worker/`(gpu_*_worker、gpu_*_model_runner、mixins)、`vllm_omni/inputs/`(runner 输入预处理:data.py、preprocess.py)和设备平台层 `vllm_omni/platforms//` +- 源码校验:以上路径均已在 `main @ 807db6ef` 验证存在;stage 配置已经迁移到 + `vllm_omni/deploy/`,NPU/XPU 等平台可继续拥有自己的 worker 覆盖 - 测试入口:共享 runner 行为看 `tests/worker/`,具体模型 consumer 看 `tests/model_executor/` - 主要职责:AR/LLM stage、stage 配置、并行与设备启动、runner 到模型的输入预处理合同和跨阶段数据桥接 diff --git a/knowledge/repos/vllm-omni/components/scheduler/_index.md b/knowledge/repos/vllm-omni/components/scheduler/_index.md index d94222d..bb192bd 100644 --- a/knowledge/repos/vllm-omni/components/scheduler/_index.md +++ b/knowledge/repos/vllm-omni/components/scheduler/_index.md @@ -11,7 +11,7 @@ sources: [vllm_omni/core/sched/omni_ar_scheduler.py, vllm_omni/core/prefix_cache - 源码入口:`vllm_omni/core/sched/`(`omni_ar_scheduler.py`、`omni_generation_scheduler.py`、 `omni_scheduler_mixin.py`、`omni_scheduling_coordinator.py`)和 `vllm_omni/core/prefix_cache.py` -- 源码校验:以上路径与下列类均已在 `main @ 5c390096` 验证存在:`OmniARScheduler`(:50)、 +- 源码校验:以上路径与下列类均已在 `main @ 807db6ef` 验证存在:`OmniARScheduler`(:50)、 `OmniARAsyncScheduler`(:928)、`KVCacheTransferData`(:40)、`OmniGenerationScheduler`(:42)、 `OmniSchedulerMixin`(:40)、`OmniTensorPrefixCache`(prefix_cache.py:33) - 官方设计文档:`docs/design/module/ar_module.md`(继承关系、请求流转图) diff --git a/knowledge/repos/vllm-omni/components/serving/_index.md b/knowledge/repos/vllm-omni/components/serving/_index.md index 1021c72..072bf42 100644 --- a/knowledge/repos/vllm-omni/components/serving/_index.md +++ b/knowledge/repos/vllm-omni/components/serving/_index.md @@ -10,7 +10,7 @@ sources: [] # Serving - 主要源码入口:`vllm_omni/entrypoints/`(cli、openai、openpi 及 omni/async_omni 入口)和 `vllm_omni/engine/`(orchestrator、stage engine core、stage pool/runtime、output processor) -- 源码校验:以上路径均已在 `main @ 238fc0a6`(此前亦在 `dev/vllm-align @ 4f2b32c` 验证,结果一致) 验证存在 +- 源码校验:以上路径均已在 `main @ 807db6ef` 验证存在 - 主要职责:用户入口、请求解析、在线服务和 engine 边界 ## 什么时候查这里 diff --git a/knowledge/repos/vllm-omni/models/catalog.md b/knowledge/repos/vllm-omni/models/catalog.md index cb61965..e5c6a5a 100644 --- a/knowledge/repos/vllm-omni/models/catalog.md +++ b/knowledge/repos/vllm-omni/models/catalog.md @@ -10,7 +10,7 @@ sources: [vllm_omni/model_executor/models/registry.py, vllm_omni/diffusion/regis # 模型代码入口与 registry 快照 本页提供模型描述到代码目录的自动定位入口,不维护逐模型 class 映射。下方计数仍是 -`main @ 5d44868e`(2026-07-21)快照,数字会漂移,不能凭它断言“不支持”。 +`v0.26.0rc1 @ 807db6ef`(2026-07-28)快照,数字会漂移,不能凭它断言“不支持”。 ## Direct 模型代码入口 @@ -35,15 +35,15 @@ adapter。已有专属知识 owner 可从 [models index](_index.md) 按名称进 | 注册点 | 位置 | 计数 | |---|---|---| -| AR/omni 架构 | `model_executor/models/registry.py` `_OMNI_MODELS` | 69 个架构名 / 26 个模型族目录 | -| Diffusion pipeline | `diffusion/registry.py` `_DIFFUSION_MODELS` | 59 条 pipeline / 35 个模型族目录 | +| AR/omni 架构 | `model_executor/models/registry.py` `_OMNI_MODELS` | 72 个架构名 / 26 个模型族目录 | +| Diffusion pipeline | `diffusion/registry.py` `_DIFFUSION_MODELS` | 61 条 pipeline / 37 个模型族目录 | | Pipeline(model_type) | `config/pipeline_registry.py` `OMNI_PIPELINES` | 46 个 key | -| Deploy YAML | `vllm_omni/deploy/*.yaml` | 65 份 | +| Deploy YAML | `vllm_omni/deploy/*.yaml` | 71 份 | -对比上一快照(`5c390096`,2026-07-16):diffusion pipeline 58→59, -OMNI_PIPELINES 39→46,deploy 58→65——新增的 key 集中在视频/歌声/机器人 -(`hunyuan_video_15`、`wan2_2_ti2v`、`soulxsinger_svs/svc`、 -`step_audio_2`、`step_audio_2_asr`、`Gr00tN1d7`),多 stage 化趋势在继续。 +对比上一审计快照(`5d44868e`,2026-07-21):AR 架构 69→72,diffusion +pipeline 59→61,OMNI_PIPELINES 保持 46,deploy 65→71。新增 diffusion +家族是 `boogu_image` 和 `lingbot_video`;AR 新增项属于已有的 +`mammoth_moda2` 与 `minicpmo_4_5` 家族。 ## AR/omni 模型族(26) @@ -53,12 +53,12 @@ mammoth_moda2、mimo_audio、ming_flash_omni、ming_tts、minicpmo_4_5、moss_tt moss_tts_nano、omnivoice、qwen2_5_omni、qwen3_omni、qwen3_tts、step_audio2、 voxcpm2、voxtral_tts -## Diffusion 模型族(35) +## Diffusion 模型族(37) -audiox、bagel、cosmos3、diffusers_adapter(通用 diffusers 桥)、dreamid_omni、 +audiox、bagel、boogu_image、cosmos3、diffusers_adapter(通用 diffusers 桥)、dreamid_omni、 dreamzero、ernie_image、flux、flux2、flux2_klein、glm_image、gr00t、helios、 hidream_image、hunyuan_image3、hunyuan_video、internvla_a1、krea2、lance、 -longcat_image、ltx2、magi_human、ming_flash_omni、nextstep_1_1、omnigen2、 +lingbot_video、longcat_image、ltx2、magi_human、ming_flash_omni、nextstep_1_1、omnigen2、 omnivoice、ovis_image、qwen_image、sd3、sdxl、sensenova_u1、soulx_singer、 stable_audio、wan2_2、z_image @@ -84,10 +84,16 @@ voxtral_tts、wan2_2_ti2v ## 重派生方法 ```bash -git -C fetch origin main -git -C show origin/main:vllm_omni/diffusion/registry.py | grep -c '":' -# 或在 python 里 import 两个 registry 与 OMNI_PIPELINES 计数 +python tools/audit_vllm_omni_release.py \ + --from 5d44868e \ + --to v0.26.0rc1 \ + --repo \ + --mode report-only ``` -有专属沉淀页的模型见 [models/_index](_index.md)(2026-07-21 起全部家族均有 -落脚页);参照用途见 [reference-models](reference-models.md)。 +命令从 Git 对象读取 registry,不 import vLLM;机器基线与完整维护步骤见 +`adapters/vllm_omni/release_baseline.yaml` 和 +`doc/VLLM_OMNI_RELEASE_MAINTENANCE.md`。 + +已有专属沉淀页的模型见 [models/_index](_index.md);没有专属规则的新家族先走共享 +Diffusion owner。参照用途见 [reference-models](reference-models.md)。 diff --git a/knowledge/repos/vllm-omni/models/voxtral-tts/_index.md b/knowledge/repos/vllm-omni/models/voxtral-tts/_index.md index 44b8f2e..987113b 100644 --- a/knowledge/repos/vllm-omni/models/voxtral-tts/_index.md +++ b/knowledge/repos/vllm-omni/models/voxtral-tts/_index.md @@ -1,15 +1,15 @@ --- title: "Voxtral TTS(Mistral 两 stage 流式 TTS)" created: 2026-07-21 -updated: 2026-07-21 +updated: 2026-07-31 type: index tags: [vllm-omni, models] -sources: [vllm_omni/model_executor/models/voxtral_tts/, vllm_omni/deploy/voxtral_tts.yaml, vllm_omni/platforms/xpu/stage_configs/voxtral_tts.yaml] +sources: [vllm_omni/model_executor/models/voxtral_tts/, vllm_omni/deploy/voxtral_tts.yaml] --- # Voxtral TTS -以下事实在 `main @ 5d44868e` 复核(源码派生页,尚无本模型的运行经验沉淀)。 +以下事实在 `main @ 807db6ef` 复核(源码派生页,尚无本模型的运行经验沉淀)。 ## 名称与范围 @@ -33,12 +33,11 @@ sources: [vllm_omni/model_executor/models/voxtral_tts/, vllm_omni/deploy/voxtral SharedMemoryConnector(`codec_chunk_frames: 25`,起始 5);**两 stage 的 `tokenizer_mode/config_format/load_format` 都是 `mistral` 且必须一致** (YAML 注释);stage 0 采样 `extra_args.cfg_alpha: 1.2`;标注 1×H20 验证。 -- **尾部家族中唯一有 XPU stage 配置**: - `vllm_omni/platforms/xpu/stage_configs/voxtral_tts.yaml` - (XPUARWorker + OmniARScheduler)。 +- XPU override 已从独立 stage config 合并到 `vllm_omni/deploy/voxtral_tts.yaml` + 的 `platforms.xpu.stages`,不再维护第二份拓扑。 ## 什么时候查这里 - 审查 voxtral_tts 的 mistral 加载三件套一致性、acoustic CUDA-graph 或 - async-chunk 改动;XPU 路径回归先查这里的 stage 配置。 + async-chunk 改动;XPU 路径回归检查同一 deploy YAML 的 platform override。 - 语义验收见 [model-validation](../../review/guides/model-validation.md)。 diff --git a/test/test_release_audit.py b/test/test_release_audit.py new file mode 100644 index 0000000..baba469 --- /dev/null +++ b/test/test_release_audit.py @@ -0,0 +1,355 @@ +from __future__ import annotations + +import json +import hashlib +import subprocess +from pathlib import Path + +import yaml + +from tools.vllm_omni_release_audit import ( + GitSnapshotReader, + audit_release, + main, + snapshot_inventory, +) + + +def _git(repo: Path, *args: str) -> str: + return subprocess.run( + ["git", *args], + cwd=repo, + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ).stdout.strip() + + +def _commit(repo: Path, message: str) -> str: + _git(repo, "add", ".") + _git( + repo, + "-c", + "user.name=Release Audit Test", + "-c", + "user.email=release-audit@example.com", + "commit", + "-m", + message, + ) + return _git(repo, "rev-parse", "HEAD") + + +def _write_upstream(repo: Path, *, second: bool) -> None: + files = { + "vllm_omni/model_executor/models/registry.py": ( + '_OMNI_MODELS = {"OldArch": ("old", "model", "Old")' + + (', "NewArch": ("new", "model", "New")' if second else "") + + "}\n" + ), + "vllm_omni/diffusion/registry.py": ( + '_DIFFUSION_MODELS = {"OldPipeline": ("old", "pipe", "Old")' + + (', "NewPipeline": ("new", "pipe", "New")' if second else "") + + "}\n" + ), + "vllm_omni/config/pipeline_registry.py": ( + 'OMNI_PIPELINES: dict[str, object] = {"old": OLD' + + (', "new": NEW' if second else "") + + "}\n" + ), + ( + "vllm_omni/deploy/renamed.yaml" if second else "vllm_omni/deploy/old.yaml" + ): "model: old\n", + "vllm_omni/model_executor/worker.py": "VALUE = 2\n" + if second + else "VALUE = 1\n", + } + if second: + files["vllm_omni/deploy/new.yaml"] = "model: new\n" + files["vllm_omni/diffusion/new.py"] = "PIPELINE = 'new'\n" + for rel, content in files.items(): + path = repo / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _fixture(tmp_path: Path) -> dict[str, Path | str]: + upstream = tmp_path / "upstream" + upstream.mkdir() + _git(upstream, "init", "-b", "main") + _write_upstream(upstream, second=False) + stale = upstream / "vllm_omni" / "model_executor" / "removed.py" + stale.write_text("OLD = True\n", encoding="utf-8") + old_sha = _commit(upstream, "old") + _write_upstream(upstream, second=True) + stale.unlink() + (upstream / "vllm_omni" / "deploy" / "old.yaml").unlink() + new_sha = _commit(upstream, "new") + + project = tmp_path / "project" + knowledge = project / "knowledge" + page = knowledge / "repos" / "vllm-omni" / "components" / "owner" / "rules.md" + page.parent.mkdir(parents=True) + page.write_text( + "---\n" + 'title: "owner"\n' + "sources: [vllm_omni/model_executor/removed.py, " + "vllm_omni/deploy/old.yaml]\n" + "---\n\n# Owner\n", + encoding="utf-8", + ) + pin = project / "PIN.md" + pin.parent.mkdir(parents=True, exist_ok=True) + pin.write_text(f"verified against main @ {new_sha[:8]}\n", encoding="utf-8") + + manifest_path = project / "manifest.yaml" + manifest_path.write_text( + yaml.safe_dump( + { + "name": "vllm_omni", + "repo": {"path": "unused"}, + "modules": { + "model_executor": {"local_paths": ["vllm_omni/model_executor/"]} + }, + }, + sort_keys=False, + ), + encoding="utf-8", + ) + inventory = snapshot_inventory(GitSnapshotReader(upstream), new_sha) + baseline_path = project / "release_baseline.yaml" + baseline_path.write_text( + yaml.safe_dump( + { + "schema_version": 1, + "upstream": { + "repository": "test/upstream", + "previous_audited_sha": old_sha, + "audited_sha": new_sha, + }, + "inventories": { + name: { + "count": len(value), + "sha256": hashlib.sha256( + json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest(), + } + for name, value in inventory.items() + }, + "path_owners": { + "configuration": [ + "vllm_omni/config/", + "vllm_omni/deploy/", + ], + "diffusion": ["vllm_omni/diffusion/"], + "model_executor": ["vllm_omni/model_executor/"], + }, + "owner_documents": { + "configuration": ["PIN.md"], + "diffusion": ["PIN.md"], + "model_executor": ["PIN.md"], + }, + "ignored_paths": [], + "pin_documents": ["PIN.md"], + }, + sort_keys=False, + ), + encoding="utf-8", + ) + return { + "upstream": upstream, + "project": project, + "knowledge": knowledge, + "manifest": manifest_path, + "baseline": baseline_path, + "old": old_sha, + "new": new_sha, + } + + +def _audit(fixture: dict[str, Path | str]): + return audit_release( + upstream_repo=fixture["upstream"], + from_ref=str(fixture["old"]), + to_ref=str(fixture["new"]), + baseline_path=fixture["baseline"], + adapter_manifest_path=fixture["manifest"], + knowledge_root=fixture["knowledge"], + project_root=fixture["project"], + ) + + +def test_audit_reports_registry_deploy_and_adapter_drift(tmp_path): + fixture = _fixture(tmp_path) + report = _audit(fixture) + + deltas = report.data["inventory"]["deltas"] + assert deltas["autoregressive"]["added"] == ["NewArch"] + assert deltas["diffusion"]["added"] == ["NewPipeline"] + assert deltas["pipelines"]["added"] == ["new"] + assert deltas["deploy_yamls"]["added"] == [ + "vllm_omni/deploy/new.yaml", + "vllm_omni/deploy/renamed.yaml", + ] + assert deltas["deploy_yamls"]["removed"] == ["vllm_omni/deploy/old.yaml"] + assert any( + change["status"].startswith("R") + and change["old_path"] == "vllm_omni/deploy/old.yaml" + and change["path"] == "vllm_omni/deploy/renamed.yaml" + for change in report.data["paths"]["changes"] + ) + assert "vllm_omni/diffusion/new.py" in report.data["paths"]["adapter_uncovered"] + + +def test_removed_active_knowledge_source_fails(tmp_path): + fixture = _fixture(tmp_path) + report = _audit(fixture) + + stale = [ + issue + for issue in report.data["issues"] + if issue["kind"] == "stale_knowledge_source" + ] + assert stale == [ + { + "kind": "stale_knowledge_source", + "document": "knowledge/repos/vllm-omni/components/owner/rules.md", + "source": "vllm_omni/deploy/old.yaml", + "renamed_to": "vllm_omni/deploy/renamed.yaml", + }, + { + "kind": "stale_knowledge_source", + "document": "knowledge/repos/vllm-omni/components/owner/rules.md", + "source": "vllm_omni/model_executor/removed.py", + }, + ] + + +def test_unmatched_path_is_a_clear_failure(tmp_path): + fixture = _fixture(tmp_path) + upstream = fixture["upstream"] + path = upstream / "unowned" + path.mkdir() + (path / "file.txt").write_text("new\n", encoding="utf-8") + fixture["new"] = _commit(upstream, "unmatched") + + report = _audit(fixture) + + assert any( + issue == {"kind": "unmatched_path", "path": "unowned/file.txt"} + for issue in report.data["issues"] + ) + + +def test_enforced_baseline_requires_the_previous_audited_sha(tmp_path): + fixture = _fixture(tmp_path) + + report = audit_release( + upstream_repo=fixture["upstream"], + from_ref=str(fixture["new"]), + to_ref=str(fixture["new"]), + baseline_path=fixture["baseline"], + adapter_manifest_path=fixture["manifest"], + knowledge_root=fixture["knowledge"], + project_root=fixture["project"], + ) + + assert any( + issue["kind"] == "baseline_from_mismatch" for issue in report.data["issues"] + ) + + +def test_overlapping_path_owners_are_suspicious(tmp_path): + fixture = _fixture(tmp_path) + baseline = yaml.safe_load(Path(fixture["baseline"]).read_text(encoding="utf-8")) + baseline["path_owners"]["second_diffusion_owner"] = ["vllm_omni/diffusion/"] + baseline["owner_documents"]["second_diffusion_owner"] = ["PIN.md"] + Path(fixture["baseline"]).write_text( + yaml.safe_dump(baseline, sort_keys=False), + encoding="utf-8", + ) + + report = _audit(fixture) + + assert any( + issue["kind"] == "suspicious_path_route" + and issue["path"] == "vllm_omni/diffusion/new.py" + for issue in report.data["issues"] + ) + + +def test_stale_source_map_path_and_pin_fail(tmp_path): + fixture = _fixture(tmp_path) + baseline = yaml.safe_load(Path(fixture["baseline"]).read_text(encoding="utf-8")) + baseline["path_owners"]["removed_owner"] = ["vllm_omni/removed_owner/"] + baseline["owner_documents"]["removed_owner"] = ["PIN.md"] + Path(fixture["baseline"]).write_text( + yaml.safe_dump(baseline, sort_keys=False), + encoding="utf-8", + ) + Path(fixture["project"], "PIN.md").write_text( + "verified against main @ 00000000\n", + encoding="utf-8", + ) + + report = _audit(fixture) + kinds = {issue["kind"] for issue in report.data["issues"]} + + assert {"stale_source_map_path", "stale_pin"} <= kinds + + +def test_report_json_is_deterministic_and_audit_does_not_edit_inputs(tmp_path): + fixture = _fixture(tmp_path) + upstream_before = _git(fixture["upstream"], "status", "--porcelain") + project_files_before = sorted( + path.relative_to(fixture["project"]).as_posix() + for path in fixture["project"].rglob("*") + if path.is_file() + ) + + first = _audit(fixture).to_json() + second = _audit(fixture).to_json() + + assert first == second + assert _git(fixture["upstream"], "status", "--porcelain") == upstream_before + assert ( + sorted( + path.relative_to(fixture["project"]).as_posix() + for path in fixture["project"].rglob("*") + if path.is_file() + ) + == project_files_before + ) + + +def test_cli_modes_and_json_output(tmp_path): + fixture = _fixture(tmp_path) + output = tmp_path / "reports" / "audit.json" + common = [ + "--from", + str(fixture["old"]), + "--to", + str(fixture["new"]), + "--repo", + str(fixture["upstream"]), + "--baseline", + str(fixture["baseline"]), + "--adapter-manifest", + str(fixture["manifest"]), + "--knowledge-root", + str(fixture["knowledge"]), + "--project-root", + str(fixture["project"]), + "--json-output", + str(output), + ] + + assert main(common + ["--mode", "enforce"]) == 1 + assert main(common + ["--mode", "report-only"]) == 0 + assert json.loads(output.read_text(encoding="utf-8"))["result"] == "drift" diff --git a/tools/audit_vllm_omni_release.py b/tools/audit_vllm_omni_release.py new file mode 100644 index 0000000..d0ac0fd --- /dev/null +++ b/tools/audit_vllm_omni_release.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Repository entrypoint for the vLLM-Omni release audit.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from tools.vllm_omni_release_audit import main # noqa: E402 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/vllm_omni_release_audit.py b/tools/vllm_omni_release_audit.py new file mode 100644 index 0000000..9c9583d --- /dev/null +++ b/tools/vllm_omni_release_audit.py @@ -0,0 +1,751 @@ +"""Deterministic, read-only drift audit for vLLM-Omni releases.""" + +from __future__ import annotations + +import argparse +import ast +import fnmatch +import hashlib +import json +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + +import yaml + + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_REGISTRIES = { + "autoregressive": ( + "vllm_omni/model_executor/models/registry.py", + "_OMNI_MODELS", + True, + ), + "diffusion": ( + "vllm_omni/diffusion/registry.py", + "_DIFFUSION_MODELS", + True, + ), + "pipelines": ( + "vllm_omni/config/pipeline_registry.py", + "OMNI_PIPELINES", + False, + ), +} +_UPSTREAM_SOURCE_PREFIXES = ( + ".buildkite/", + "benchmarks/", + "docs/", + "tests/", + "vllm_omni/", +) +_SOURCE_EXCLUDED_PARTS = {"_archive", "history", "incidents", "results"} +_PIN_RE = re.compile( + r"\b(?:main|v\d[0-9A-Za-z.-]*)\s*@\s*`?([0-9a-f]{8,40})", + re.IGNORECASE, +) + + +class ReleaseAuditError(RuntimeError): + """The audit could not read or understand one of its declared inputs.""" + + +@dataclass(frozen=True) +class ChangedPath: + status: str + path: str + old_path: str | None = None + + def as_dict(self) -> dict[str, str]: + item = {"status": self.status, "path": self.path} + if self.old_path: + item["old_path"] = self.old_path + return item + + +@dataclass(frozen=True) +class ReleaseAuditReport: + data: dict[str, Any] + + @property + def has_drift(self) -> bool: + return bool(self.data["issues"]) + + def to_json(self) -> str: + return ( + json.dumps( + self.data, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + + "\n" + ) + + +class GitSnapshotReader: + """Read immutable Git objects without checking out or importing upstream.""" + + def __init__(self, repo: Path | str): + self.repo = Path(repo).resolve() + if not self.repo.is_dir(): + raise ReleaseAuditError(f"upstream checkout does not exist: {self.repo}") + self._run_text("rev-parse", "--git-dir") + + def _run( + self, + *args: str, + text: bool, + check: bool = True, + ) -> subprocess.CompletedProcess: + try: + result = subprocess.run( + ["git", "-C", str(self.repo), *args], + capture_output=True, + text=text, + encoding="utf-8" if text else None, + errors="replace" if text else None, + timeout=60, + check=False, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ReleaseAuditError(f"git {' '.join(args)} failed: {exc}") from exc + if check and result.returncode: + stderr = ( + result.stderr.strip() + if text + else result.stderr.decode("utf-8", errors="replace").strip() + ) + raise ReleaseAuditError( + f"git {' '.join(args)} failed ({result.returncode}): {stderr}" + ) + return result + + def _run_text(self, *args: str, check: bool = True) -> str: + return self._run(*args, text=True, check=check).stdout.strip() + + def resolve(self, ref: str) -> str: + sha = self._run_text("rev-parse", "--verify", f"{ref}^{{commit}}") + if not re.fullmatch(r"[0-9a-f]{40}", sha): + raise ReleaseAuditError(f"ref did not resolve to a commit: {ref}") + return sha + + def read_text(self, commit: str, path: str) -> str: + return self._run_text("show", f"{commit}:{path}") + + def tree_files(self, commit: str) -> list[str]: + raw = self._run( + "ls-tree", + "-r", + "--name-only", + "-z", + commit, + text=False, + ).stdout + return sorted( + part.decode("utf-8", errors="surrogateescape") + for part in raw.split(b"\0") + if part + ) + + def changed_paths(self, old: str, new: str) -> list[ChangedPath]: + raw = self._run( + "diff", + "--name-status", + "-z", + "-M", + old, + new, + "--", + text=False, + ).stdout + fields = [ + part.decode("utf-8", errors="surrogateescape") + for part in raw.split(b"\0") + if part + ] + changed: list[ChangedPath] = [] + index = 0 + while index < len(fields): + status = fields[index] + index += 1 + kind = status[:1] + if kind in {"R", "C"}: + if index + 1 >= len(fields): + raise ReleaseAuditError("unexpected truncated git rename output") + old_path, new_path = fields[index], fields[index + 1] + index += 2 + changed.append( + ChangedPath(status=status, path=new_path, old_path=old_path) + ) + else: + if index >= len(fields): + raise ReleaseAuditError("unexpected truncated git diff output") + changed.append(ChangedPath(status=status, path=fields[index])) + index += 1 + return sorted(changed, key=lambda item: (item.path, item.old_path or "")) + + +def _assigned_dict(source: str, variable: str, path: str) -> ast.Dict: + try: + tree = ast.parse(source, filename=path) + except SyntaxError as exc: + raise ReleaseAuditError(f"cannot parse {path}: {exc}") from exc + for node in tree.body: + value: ast.AST | None = None + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == variable + for target in node.targets + ): + value = node.value + elif ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == variable + ): + value = node.value + if value is not None: + if not isinstance(value, ast.Dict): + raise ReleaseAuditError(f"{path}:{variable} is not a dict literal") + return value + raise ReleaseAuditError(f"{path} does not define {variable}") + + +def _registry_entries( + source: str, + variable: str, + path: str, + include_targets: bool, +) -> dict[str, list[str]] | list[str]: + dictionary = _assigned_dict(source, variable, path) + entries: dict[str, list[str]] = {} + keys: list[str] = [] + for key_node, value_node in zip(dictionary.keys, dictionary.values): + if not isinstance(key_node, ast.Constant) or not isinstance( + key_node.value, str + ): + raise ReleaseAuditError(f"{path}:{variable} has a non-string key") + key = key_node.value + if key in keys: + raise ReleaseAuditError(f"{path}:{variable} has duplicate key {key!r}") + keys.append(key) + if include_targets: + try: + value = ast.literal_eval(value_node) + except (ValueError, TypeError, SyntaxError) as exc: + raise ReleaseAuditError( + f"{path}:{variable}[{key!r}] is not a literal target tuple" + ) from exc + if ( + not isinstance(value, tuple) + or len(value) != 3 + or not all(isinstance(part, str) for part in value) + ): + raise ReleaseAuditError( + f"{path}:{variable}[{key!r}] is not a 3-string target tuple" + ) + entries[key] = list(value) + return dict(sorted(entries.items())) if include_targets else sorted(keys) + + +def _fingerprint(value: Any) -> dict[str, int | str]: + encoded = json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + return { + "count": len(value), + "sha256": hashlib.sha256(encoded).hexdigest(), + } + + +def snapshot_inventory(reader: GitSnapshotReader, commit: str) -> dict[str, Any]: + inventories: dict[str, Any] = {} + for name, (path, variable, include_targets) in _REGISTRIES.items(): + inventories[name] = _registry_entries( + reader.read_text(commit, path), + variable, + path, + include_targets, + ) + inventories["deploy_yamls"] = sorted( + path + for path in reader.tree_files(commit) + if path.startswith("vllm_omni/deploy/") + and path.casefold().endswith((".yaml", ".yml")) + ) + return inventories + + +def _inventory_delta(old: Any, new: Any) -> dict[str, Any]: + if isinstance(old, dict) and isinstance(new, dict): + old_keys, new_keys = set(old), set(new) + return { + "added": sorted(new_keys - old_keys), + "removed": sorted(old_keys - new_keys), + "changed": sorted( + key for key in old_keys & new_keys if old[key] != new[key] + ), + } + old_set, new_set = set(old), set(new) + return { + "added": sorted(new_set - old_set), + "removed": sorted(old_set - new_set), + "changed": [], + } + + +def _load_yaml_mapping(path: Path, label: str) -> dict[str, Any]: + try: + value = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError) as exc: + raise ReleaseAuditError(f"cannot read {label} {path}: {exc}") from exc + if not isinstance(value, dict): + raise ReleaseAuditError(f"{label} must be a YAML mapping: {path}") + return value + + +def _matches(path: str, pattern: str) -> bool: + normalized = pattern.replace("\\", "/") + if normalized.endswith("/"): + return path.startswith(normalized) + return fnmatch.fnmatchcase(path, normalized) + + +def _route_path(path: str, baseline: dict[str, Any]) -> tuple[list[str], list[str]]: + owners = sorted( + owner + for owner, patterns in (baseline.get("path_owners") or {}).items() + if any(_matches(path, str(pattern)) for pattern in patterns or []) + ) + ignored = sorted( + str(item.get("reason") or "no reason supplied") + for item in baseline.get("ignored_paths") or [] + if isinstance(item, dict) and _matches(path, str(item.get("pattern") or "")) + ) + return owners, ignored + + +def _adapter_modules(path: str, manifest: dict[str, Any]) -> list[str]: + modules: list[str] = [] + for module, spec in (manifest.get("modules") or {}).items(): + for pattern in (spec or {}).get("local_paths") or []: + prefix = str(pattern).replace("\\", "/").rstrip("*").rstrip("/") + if path.startswith(prefix): + modules.append(str(module)) + break + return sorted(modules) + + +def _source_path(value: str) -> str | None: + candidate = value.strip().strip("`") + if not candidate.startswith(_UPSTREAM_SOURCE_PREFIXES): + return None + candidate = candidate.split("::", 1)[0] + candidate = re.sub(r":\d+(?:-\d+)?$", "", candidate) + return candidate + + +def _path_exists(files: Iterable[str], source_path: str) -> bool: + file_list = files if isinstance(files, list) else list(files) + if any(char in source_path for char in "*?["): + return any(fnmatch.fnmatchcase(path, source_path) for path in file_list) + if source_path.endswith("/"): + return any(path.startswith(source_path) for path in file_list) + return source_path in file_list or any( + path.startswith(source_path.rstrip("/") + "/") for path in file_list + ) + + +def _knowledge_source_issues( + knowledge_root: Path, + old_files: list[str], + new_files: list[str], + changes: list[ChangedPath], +) -> list[dict[str, str]]: + repo_root = knowledge_root / "repos" / "vllm-omni" + if not repo_root.is_dir(): + raise ReleaseAuditError(f"vLLM-Omni knowledge root is missing: {repo_root}") + rename_map = { + item.old_path: item.path + for item in changes + if item.old_path and item.status.startswith("R") + } + issues: list[dict[str, str]] = [] + for page in sorted(repo_root.rglob("*.md")): + if any( + part in _SOURCE_EXCLUDED_PARTS for part in page.relative_to(repo_root).parts + ): + continue + text = page.read_text(encoding="utf-8") + if not text.startswith("---"): + continue + parts = text.split("---", 2) + if len(parts) != 3: + continue + try: + metadata = yaml.safe_load(parts[1]) or {} + except yaml.YAMLError as exc: + issues.append( + { + "kind": "knowledge_metadata_error", + "document": page.relative_to(knowledge_root.parent).as_posix(), + "detail": str(exc).splitlines()[0], + } + ) + continue + sources = metadata.get("sources") or [] + if not isinstance(sources, list): + issues.append( + { + "kind": "knowledge_metadata_error", + "document": page.relative_to(knowledge_root.parent).as_posix(), + "detail": "sources must be a list", + } + ) + continue + for raw_source in sources: + if not isinstance(raw_source, str): + continue + source_path = _source_path(raw_source) + if not source_path: + continue + if _path_exists(old_files, source_path) and not _path_exists( + new_files, source_path + ): + issue = { + "kind": "stale_knowledge_source", + "document": page.relative_to(knowledge_root.parent).as_posix(), + "source": source_path, + } + if source_path in rename_map: + issue["renamed_to"] = rename_map[source_path] + issues.append(issue) + return sorted( + issues, + key=lambda item: ( + item["kind"], + item.get("document", ""), + item.get("source", ""), + ), + ) + + +def _pin_issues( + project_root: Path, + baseline: dict[str, Any], + audited_sha: str, +) -> list[dict[str, str]]: + issues: list[dict[str, str]] = [] + for rel in baseline.get("pin_documents") or []: + document = project_root / str(rel) + if not document.is_file(): + issues.append( + { + "kind": "pin_document_missing", + "document": str(rel), + } + ) + continue + pins = sorted(set(_PIN_RE.findall(document.read_text(encoding="utf-8")))) + if not pins: + issues.append( + { + "kind": "pin_marker_missing", + "document": str(rel), + } + ) + continue + for pin in pins: + if not audited_sha.startswith(pin): + issues.append( + { + "kind": "stale_pin", + "document": str(rel), + "actual": pin, + "expected": audited_sha, + } + ) + return issues + + +def _policy_issues( + project_root: Path, + baseline: dict[str, Any], + target_files: list[str], +) -> list[dict[str, str]]: + issues: list[dict[str, str]] = [] + owner_documents = baseline.get("owner_documents") or {} + for owner, patterns in sorted((baseline.get("path_owners") or {}).items()): + documents = owner_documents.get(owner) or [] + if not documents: + issues.append({"kind": "owner_document_missing", "owner": str(owner)}) + for document in documents: + if not (project_root / str(document)).is_file(): + issues.append( + { + "kind": "owner_document_missing", + "owner": str(owner), + "document": str(document), + } + ) + for pattern in patterns or []: + if not _path_exists(target_files, str(pattern)): + issues.append( + { + "kind": "stale_source_map_path", + "owner": str(owner), + "path": str(pattern), + } + ) + return issues + + +def audit_release( + *, + upstream_repo: Path | str, + from_ref: str, + to_ref: str, + baseline_path: Path | str, + adapter_manifest_path: Path | str, + knowledge_root: Path | str, + project_root: Path | str = _REPO_ROOT, +) -> ReleaseAuditReport: + reader = GitSnapshotReader(upstream_repo) + old_sha, new_sha = reader.resolve(from_ref), reader.resolve(to_ref) + baseline_path = Path(baseline_path) + baseline = _load_yaml_mapping(baseline_path, "release baseline") + manifest = _load_yaml_mapping(Path(adapter_manifest_path), "adapter manifest") + if baseline.get("schema_version") != 1: + raise ReleaseAuditError("release baseline schema_version must be 1") + + old_inventory = snapshot_inventory(reader, old_sha) + new_inventory = snapshot_inventory(reader, new_sha) + changes = reader.changed_paths(old_sha, new_sha) + old_files, new_files = reader.tree_files(old_sha), reader.tree_files(new_sha) + + issues: list[dict[str, Any]] = [] + expected_sha = str((baseline.get("upstream") or {}).get("audited_sha") or "") + if expected_sha != new_sha: + issues.append( + { + "kind": "baseline_pin_mismatch", + "baseline": expected_sha, + "target": new_sha, + } + ) + expected_old_sha = str( + (baseline.get("upstream") or {}).get("previous_audited_sha") or "" + ) + if expected_sha == new_sha and expected_old_sha != old_sha: + issues.append( + { + "kind": "baseline_from_mismatch", + "baseline": expected_old_sha, + "target": old_sha, + } + ) + + actual_fingerprints = { + name: _fingerprint(value) for name, value in new_inventory.items() + } + expected_fingerprints = baseline.get("inventories") or {} + for name in sorted(actual_fingerprints): + if expected_fingerprints.get(name) != actual_fingerprints[name]: + issues.append( + { + "kind": "inventory_mismatch", + "inventory": name, + "baseline": expected_fingerprints.get(name), + "target": actual_fingerprints[name], + } + ) + + routing: list[dict[str, Any]] = [] + adapter_uncovered: list[str] = [] + for change in changes: + owners, ignored = _route_path(change.path, baseline) + adapter_modules = _adapter_modules(change.path, manifest) + route = { + **change.as_dict(), + "owners": owners, + "ignored": ignored, + "adapter_modules": adapter_modules, + "knowledge_documents": sorted( + { + str(document) + for owner in owners + for document in (baseline.get("owner_documents") or {}).get( + owner, [] + ) + } + ), + } + routing.append(route) + if change.path.startswith("vllm_omni/") and not adapter_modules: + adapter_uncovered.append(change.path) + if not owners and not ignored: + issues.append({"kind": "unmatched_path", "path": change.path}) + elif len(owners) > 1 or (owners and ignored): + issues.append( + { + "kind": "suspicious_path_route", + "path": change.path, + "owners": owners, + "ignored": ignored, + } + ) + + issues.extend( + _knowledge_source_issues( + Path(knowledge_root), + old_files, + new_files, + changes, + ) + ) + issues.extend(_pin_issues(Path(project_root), baseline, new_sha)) + issues.extend( + _policy_issues( + Path(project_root), + baseline, + new_files, + ) + ) + issues = sorted( + issues, + key=lambda item: ( + str(item.get("kind", "")), + str(item.get("path", "")), + str(item.get("document", "")), + str(item.get("inventory", "")), + ), + ) + + deltas = { + name: _inventory_delta(old_inventory[name], new_inventory[name]) + for name in sorted(new_inventory) + } + status_counts: dict[str, int] = {} + for change in changes: + kind = change.status[:1] + status_counts[kind] = status_counts.get(kind, 0) + 1 + + data = { + "schema_version": 1, + "upstream": { + "repository": str((baseline.get("upstream") or {}).get("repository") or ""), + "from": {"ref": from_ref, "sha": old_sha}, + "to": {"ref": to_ref, "sha": new_sha}, + }, + "inventory": { + "deltas": deltas, + "target_fingerprints": actual_fingerprints, + }, + "paths": { + "counts": dict(sorted(status_counts.items())), + "changes": [item.as_dict() for item in changes], + "routing": routing, + "adapter_uncovered": sorted(set(adapter_uncovered)), + }, + "issues": issues, + "result": "drift" if issues else "clean", + } + return ReleaseAuditReport(data) + + +def render_summary(report: ReleaseAuditReport) -> str: + data = report.data + old = data["upstream"]["from"]["sha"][:8] + new = data["upstream"]["to"]["sha"][:8] + deltas = data["inventory"]["deltas"] + lines = [f"vLLM-Omni release audit: {old} -> {new}"] + for name in ("autoregressive", "diffusion", "pipelines", "deploy_yamls"): + delta = deltas[name] + lines.append( + f" {name}: +{len(delta['added'])} -{len(delta['removed'])} " + f"~{len(delta['changed'])}" + ) + lines.append( + f" changed paths: {len(data['paths']['changes'])}; " + "runtime-manifest gaps (reported separately): " + f"{len(data['paths']['adapter_uncovered'])}" + ) + lines.append(f" issues: {len(data['issues'])}") + for issue in data["issues"][:10]: + subject = ( + issue.get("path") or issue.get("document") or issue.get("inventory") or "" + ) + lines.append(f" - {issue['kind']}: {subject}".rstrip()) + if len(data["issues"]) > 10: + lines.append(f" ... {len(data['issues']) - 10} more in JSON report") + lines.append(f"RESULT: {data['result'].upper()}") + return "\n".join(lines) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Audit vLLM-Omni release drift without modifying either repo." + ) + parser.add_argument("--from", dest="from_ref", required=True) + parser.add_argument("--to", dest="to_ref", required=True) + parser.add_argument("--repo", required=True, type=Path) + parser.add_argument( + "--baseline", + type=Path, + default=_REPO_ROOT / "adapters" / "vllm_omni" / "release_baseline.yaml", + ) + parser.add_argument( + "--adapter-manifest", + type=Path, + default=_REPO_ROOT / "adapters" / "vllm_omni" / "manifest.yaml", + ) + parser.add_argument( + "--knowledge-root", + type=Path, + default=_REPO_ROOT / "knowledge", + ) + parser.add_argument( + "--project-root", + type=Path, + default=_REPO_ROOT, + help="root used to resolve pin_documents from the baseline", + ) + parser.add_argument("--json-output", type=Path) + parser.add_argument( + "--mode", + choices=("enforce", "report-only"), + default="enforce", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + report = audit_release( + upstream_repo=args.repo, + from_ref=args.from_ref, + to_ref=args.to_ref, + baseline_path=args.baseline, + adapter_manifest_path=args.adapter_manifest, + knowledge_root=args.knowledge_root, + project_root=args.project_root, + ) + except ReleaseAuditError as exc: + print(f"release audit error: {exc}") + return 2 + print(render_summary(report)) + if args.json_output: + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text(report.to_json(), encoding="utf-8") + print(f"JSON report: {args.json_output}") + return 1 if report.has_drift and args.mode == "enforce" else 0 + + +if __name__ == "__main__": + raise SystemExit(main())