Skip to content

Commit c56182a

Browse files
fix(matrix): pin generator imports and preserve historical callers (#3497)
修复矩阵生成器导入隔离,保留旧修订的工作流和 OperatorX 入口,并补充行为回归测试与双语文档。
1 parent 090501c commit c56182a

10 files changed

Lines changed: 138 additions & 40 deletions

File tree

‎.github/workflows/e2e-tests.yml‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,19 @@ jobs:
314314
if [ "$EVALS_ONLY" = "true" ]; then
315315
GENERATE_ARGS+=(--evals-only)
316316
fi
317-
CONFIG_JSON=$(env PYTHONPATH="${GITHUB_WORKSPACE}" \
317+
GENERATOR_PATH="${GITHUB_WORKSPACE}"
318+
if [ -f "${GITHUB_WORKSPACE}/infx/matrix/generate.py" ]; then
319+
GENERATOR=(-m infx.matrix.generate)
320+
elif [ -f "${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py" ]; then
321+
GENERATOR=("${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py")
322+
GENERATOR_PATH="${GITHUB_WORKSPACE}/utils/matrix_logic:${GITHUB_WORKSPACE}"
323+
else
324+
echo "Matrix generator not found in selected checkout" >&2
325+
exit 1
326+
fi
327+
CONFIG_JSON=$(env PYTHONPATH="$GENERATOR_PATH" \
318328
uv run --no-project --exclude-newer PT12H --python 3.12 --with pydantic --with pyyaml \
319-
python -P -m infx.matrix.generate "${GENERATE_ARGS[@]}")
329+
python -P "${GENERATOR[@]}" "${GENERATE_ARGS[@]}")
320330
fi
321331
CONFIG_JSON=$(printf '%s' "$CONFIG_JSON" | env PYTHONPATH="$PRIORITY_ROOT" \
322332
uv run --no-project --exclude-newer PT12H --python 3.12 --with pydantic --with pyyaml \

‎.github/workflows/profile.yml‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,19 @@ jobs:
8585
fi
8686
source "$PRIORITY_ROOT/benchmarks/benchmark_lib.sh" --validation-only
8787
check_env_vars INPUTS_CONFIG_FILE INPUTS_CONFIG_KEY INPUTS_CONC PR_LABELS
88-
CONFIG_JSON=$(env PYTHONPATH="${GITHUB_WORKSPACE}" \
88+
GENERATOR_PATH="${GITHUB_WORKSPACE}"
89+
if [ -f "${GITHUB_WORKSPACE}/infx/matrix/generate.py" ]; then
90+
GENERATOR=(-m infx.matrix.generate)
91+
elif [ -f "${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py" ]; then
92+
GENERATOR=("${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py")
93+
GENERATOR_PATH="${GITHUB_WORKSPACE}/utils/matrix_logic:${GITHUB_WORKSPACE}"
94+
else
95+
echo "Matrix generator not found in selected checkout" >&2
96+
exit 1
97+
fi
98+
CONFIG_JSON=$(env PYTHONPATH="$GENERATOR_PATH" \
8999
uv run --no-project --exclude-newer PT12H --python 3.12 --with pydantic --with pyyaml \
90-
python -P -m infx.matrix.generate \
100+
python -P "${GENERATOR[@]}" \
91101
test-config --config-files "$INPUTS_CONFIG_FILE" --config-keys "$INPUTS_CONFIG_KEY" --conc "$INPUTS_CONC")
92102
CONFIG_JSON=$(printf '%s' "$CONFIG_JSON" | env PYTHONPATH="$PRIORITY_ROOT" uv run --no-project --exclude-newer PT12H --python 3.12 --with pyyaml \
93103
python -P -m infx.workflows.ci_priority \

‎docs/MODELS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Other offloading tiers, including NVMe KV cache offloading, are outside the init
175175

176176
## Notes
177177

178-
- The `Prefix` column is the canonical `model-prefix` used in `configs/*-master.yaml` and by `python -m infx.matrix.generate --model-prefix`.
178+
- The `Prefix` column is the canonical `model-prefix` used in `configs/*-master.yaml` and by the `full-sweep` subcommand's `--model-prefix` filter, for example `python -m infx.matrix.generate full-sweep --config-files configs/nvidia-master.yaml --model-prefix dsr1`.
179179
- "Retired" means the model no longer has any active scenario. Retired models' configs are deleted from the master configs; the former `configs/deprecated/` archive was removed in #3464, so past settings live in git history and `perf-changelog.yaml`.
180180
- Deprecating a precision (e.g. Qwen3.5 bf16) or one arm of an A/B pair (e.g. non-MTP) narrows a model's recipe coverage without retiring the model. The model stays listed as active as long as one scenario still runs.
181181
- `dsr1` began as the DeepSeek-V3 workflow templates in the initial repo import and was switched to DeepSeek-R1 benchmarking on 2025-08-13 (renamed `dsv3` → `dsr1` on 2025-08-20).

‎docs/MODELS_zh.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ InferenceX 支持 SGLang 和 vLLM 双方的维护者,并响应 AI 实验室和
175175

176176
## 说明
177177

178-
- 「前缀」列为 `configs/*-master.yaml` 中的规范 `model-prefix`,同时用于 `python -m infx.matrix.generate --model-prefix`。
178+
- 「前缀」列为 `configs/*-master.yaml` 中的规范 `model-prefix`,同时用于 `full-sweep` 子命令的 `--model-prefix` 筛选参数,例如 `python -m infx.matrix.generate full-sweep --config-files configs/nvidia-master.yaml --model-prefix dsr1`。
179179
- 「退役」指该模型已无任何启用场景。退役模型的配置直接从主配置中删除;原 `configs/deprecated/` 归档目录已在 #3464 中删除,历史设置保留在 Git 历史与 `perf-changelog.yaml` 中。
180180
- 弃用某一精度(如 Qwen3.5 bf16)或 A/B 对照中的某一分支(如非 MTP),只是收窄该模型的配方覆盖范围,并不等于模型退役;只要仍有一个场景在运行,该模型即继续列为启用状态。
181181
- `dsr1` 最初以 DeepSeek-V3 workflow 模板的形式随仓库首次导入,2025-08-13 切换为 DeepSeek-R1 基准测试(2025-08-20 将 `dsv3` 重命名为 `dsr1`)。

‎docs/architecture.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,12 @@ Eval adapters and patches copied into isolated environments use the actual files
146146

147147
Default repository paths live in [`infx/config.py`](../infx/config.py). Import configuration constants from `infx.config` and schemas from `infx.matrix.validation`. Package `__init__.py` files stay minimal.
148148

149-
Run changelog planning with `python -m infx.matrix.plan` and validation with `python -m infx.workflows.validate_perf_changelog` from the repository root or an installed package. Matrix generation runs as `python -m infx.matrix.generate`; historical append-only planning runs a base revision's own generator, including its legacy script when the module is absent. Ingest recovery uses the recovery tool's own planner module and the selected worktree's configs and recipes.
149+
Run changelog planning with `python -m infx.matrix.plan` and validation with `python -m infx.workflows.validate_perf_changelog` from the repository root or an installed package. Matrix generation uses the `python -m infx.matrix.generate` entrypoint with a `full-sweep` or `test-config` subcommand; historical append-only planning runs a base revision's own generator, including its legacy script when the module is absent. Ingest recovery uses the recovery tool's own planner module and the selected worktree's configs and recipes.
150150

151151
Workflows using current tooling call `infx` modules directly, and tests import canonical modules. Trusted dispatch and result processing select their tooling checkout explicitly through `PYTHONPATH` and Python's `-P` option while keeping the target checkout as the working directory for inputs. Recovery sets `INFERENCEX_REPOSITORY_ROOT` to the selected worktree so recipe data comes from that revision; other callers retain the source-checkout default.
152152

153+
Manual matrix generation, profiling, OperatorX enumeration, and historical append-only planning support both module and legacy-script layouts. Their generator subprocesses explicitly put the selected checkout or snapshot on `PYTHONPATH`, including the legacy script's sibling directory when needed, so `PYTHONSAFEPATH` cannot make another installed checkout override it.
154+
153155
`infx.matrix.plan.build_plan(changelog_data, base_ref=..., head_ref=...)` returns the validated `ChangelogMatrixEntry` for the complete sweep. It owns entry precedence, separate benchmark/eval scenario coverage, trimming, fingerprints, and output buckets. Current master files are loaded once, and runner metadata is loaded once on first generation; each selected group calls `infx.matrix.generate.generate_config_matrix` directly. Current inputs come from the supplied paths (the checkout defaults), while `head_ref` remains provenance metadata. Planning assumes those files are stable during the operation.
154156

155157
`generate_config_matrix` takes validated config/runner dictionaries and an explicit eval mode (`default`, `none`, `subset`, `all`, or `smoke`). It shares expansion and eval selection with the generator CLI, including checked-in recipe reads for physical node counts. Smoke keeps minimum-concurrency throughput and separate canonical representative evals. Its JSON normalization preserves legacy value/rejection semantics and prevents nested result objects from leaking between passes. The planner publishes no partial matrix on failure; the CLI retains rejection status and diagnostic messages, while traceback frames reflect the new module locations.

‎docs/architecture_zh.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,12 @@ flowchart LR
146146

147147
默认仓库路径定义在 [`infx/config.py`](../infx/config.py) 中。配置常量从 `infx.config` 导入,模式从 `infx.matrix.validation` 导入。包的 `__init__.py` 文件保持精简。
148148

149-
从仓库根目录或安装好的包运行 `python -m infx.matrix.plan` 进行变更日志规划,运行 `python -m infx.workflows.validate_perf_changelog` 进行验证。矩阵生成通过 `python -m infx.matrix.generate` 运行;历史 append-only 规划使用基准修订版自身的生成器,缺少模块时使用该修订版的旧脚本。摄取恢复使用恢复工具自身的规划模块,以及所选 worktree 的配置和配方。
149+
从仓库根目录或安装好的包运行 `python -m infx.matrix.plan` 进行变更日志规划,运行 `python -m infx.workflows.validate_perf_changelog` 进行验证。矩阵生成使用 `python -m infx.matrix.generate` 入口,并指定 `full-sweep` 或 `test-config` 子命令;历史 append-only 规划使用基准修订版自身的生成器,缺少模块时使用该修订版的旧脚本。摄取恢复使用恢复工具自身的规划模块,以及所选 worktree 的配置和配方。
150150

151151
使用当前工具代码的工作流直接调用 `infx` 模块,测试也导入规范模块。可信调度和结果处理通过 `PYTHONPATH` 和 Python 的 `-P` 选项明确指定工具代码所在的检出目录,同时仍以目标检出目录作为工作目录读取输入。恢复工具将 `INFERENCEX_REPOSITORY_ROOT` 设为所选 worktree,确保配方数据来自该修订版;其他调用方仍默认使用源码检出目录。
152152

153+
手动矩阵生成、性能分析、OperatorX 枚举和历史 append-only 规划均支持模块与遗留脚本两种布局。生成器子进程会将所选检出目录或快照明确置于 `PYTHONPATH` 前部,必要时也包含遗留脚本所在目录,因此启用 `PYTHONSAFEPATH` 时也不会误用其他已安装检出版本的代码。
154+
153155
`infx.matrix.plan.build_plan(changelog_data, base_ref=..., head_ref=...)` 返回完整扫描的已验证 `ChangelogMatrixEntry`,统一负责条目优先级、基准测试与评测各自的场景覆盖、裁剪、指纹及输出分桶。当前主配置文件只加载一次,运行器元数据在首次生成时加载一次;每组选中的配置直接调用 `infx.matrix.generate.generate_config_matrix`。当前输入来自传入的路径(默认为检出目录中的路径),`head_ref` 仍用作来源元数据。规划过程假设这些文件在本次操作期间保持稳定。
154156

155157
`generate_config_matrix` 接受已验证的配置和运行器字典,以及明确的评测模式(`default`、`none`、`subset`、`all` 或 `smoke`),与生成器 CLI 共用展开和评测选择逻辑,包括读取仓库中已提交的配方以确定物理节点数。Smoke 保留最低并发吞吐量测试,并单独保留规范选出的代表性评测。其 JSON 规范化保留旧有值转换和拒绝语义,并防止不同生成批次共享嵌套结果对象。规划失败时不会发布不完整矩阵;CLI 保留拒绝状态和诊断消息,堆栈帧则反映新的模块位置。

‎experimental/operatorx/scripts/inferencex_testlist/matrix.py‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,30 @@ def _run_inferencex_generator(
5757
if not os.path.isdir(inferencex_dir):
5858
raise FileNotFoundError(f"InferenceX directory not found: {inferencex_dir}")
5959

60+
inferencex_dir = os.path.abspath(inferencex_dir)
6061
module_path = os.path.join(inferencex_dir, "infx", "matrix", "generate.py")
61-
if not os.path.isfile(module_path):
62+
legacy_script = os.path.join(inferencex_dir, "utils", "matrix_logic", "generate_sweep_configs.py")
63+
import_paths = [inferencex_dir]
64+
if os.path.isfile(module_path):
65+
generator = ["-m", "infx.matrix.generate"]
66+
elif os.path.isfile(legacy_script):
67+
generator = [legacy_script]
68+
import_paths.insert(0, os.path.dirname(legacy_script))
69+
else:
6270
raise FileNotFoundError(f"InferenceX generator not found: {module_path}")
6371

64-
# Running from the InferenceX root puts its infx package first on sys.path.
65-
args = [sys.executable, "-m", "infx.matrix.generate", "full-sweep", "--config-files", *config_files]
72+
# Explicit paths also pin the selected revision with PYTHONSAFEPATH enabled.
73+
env = os.environ.copy()
74+
if env.get("PYTHONPATH"):
75+
import_paths.append(env["PYTHONPATH"])
76+
env["PYTHONPATH"] = os.pathsep.join(import_paths)
77+
args = [sys.executable, *generator, "full-sweep", "--config-files", *config_files]
6678
if extra_flags:
6779
args.extend(extra_flags)
6880

6981
# Run from the InferenceX root so relative paths in the master config resolve.
7082
proc = subprocess.run(
71-
args, capture_output=True, text=True, cwd=inferencex_dir, check=False
83+
args, capture_output=True, text=True, cwd=inferencex_dir, env=env, check=False
7284
)
7385
if proc.returncode != 0:
7486
raise RuntimeError(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Check InferenceX generator subprocesses against selected checkout layouts."""
2+
3+
import os
4+
import shutil
5+
import sys
6+
from pathlib import Path
7+
8+
import pytest
9+
import yaml
10+
11+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
12+
from operatorx.scripts.inferencex_testlist.matrix import _run_inferencex_generator
13+
14+
15+
@pytest.mark.parametrize("legacy", [False, True])
16+
def test_generator_uses_requested_checkout_with_safe_path(tmp_path, monkeypatch, legacy):
17+
root = tmp_path / "selected checkout"
18+
root.mkdir()
19+
foreign = tmp_path / "other checkout"
20+
(foreign / "infx").mkdir(parents=True)
21+
(foreign / "infx/__init__.py").write_text("raise RuntimeError('wrong checkout')\n")
22+
monkeypatch.setenv("PYTHONPATH", str(foreign))
23+
monkeypatch.setenv("PYTHONSAFEPATH", "1")
24+
monkeypatch.chdir(tmp_path)
25+
26+
if legacy:
27+
# The old generator is an external executable. Its sibling dependency
28+
# distinguishes the selected revision from inherited import paths.
29+
script = root / "utils/matrix_logic/generate_sweep_configs.py"
30+
script.parent.mkdir(parents=True)
31+
script.write_text(
32+
"import json, sys\nfrom validation import model\n"
33+
"assert sys.argv[1:] == ['full-sweep', '--config-files', 'master.yaml', '--no-evals']\n"
34+
"print(json.dumps([{'model': model, 'conc': 2}]))\n"
35+
)
36+
script.with_name("validation.py").write_text("model = 'legacy revision'\n")
37+
expected = "legacy revision"
38+
else:
39+
source = Path(__file__).resolve().parents[3]
40+
shutil.copytree(source / "infx", root / "infx",
41+
ignore=shutil.ignore_patterns("__pycache__", "tests"))
42+
(root / "configs").mkdir()
43+
(root / "configs/runners.yaml").write_text("labels: {fixture: [node-a]}\nhardware: {}\n")
44+
(root / "master.yaml").write_text(yaml.safe_dump({"fixture": {
45+
"image": "example/image:stable", "model": "selected revision",
46+
"model-prefix": "dsr1", "precision": "fp8", "framework": "sglang",
47+
"runner": "fixture", "multinode": False,
48+
"scenarios": {"fixed-seq-len": [{
49+
"isl": 1024, "osl": 1024,
50+
"search-space": [{"tp": 1, "conc-list": [2]}],
51+
}]},
52+
}}))
53+
expected = "selected revision"
54+
55+
rows = _run_inferencex_generator(os.path.relpath(root), ["master.yaml"], ["--no-evals"])
56+
assert [(row["model"], row["conc"]) for row in rows] == [(expected, 2)]

‎infx/matrix/plan.py‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class GenerationInputs:
4949
# argv after python3: ("-m", module) or (legacy script path,)
5050
generator: tuple[str, ...]
5151
runner_config: str
52-
# Child working directory; a snapshot root makes `-m` import its own infx.
52+
# Child working directory and explicit import root for snapshot isolation.
5353
root: str | None = None
5454

5555

@@ -396,16 +396,20 @@ def generate_matrix(
396396
legacy flags, optional input override, and child diagnostics for script users.
397397
"""
398398
command = _matrix_command(config_keys, flags, inputs)
399+
root = Path(inputs.root).resolve() if inputs and inputs.root else Path.cwd()
400+
import_paths = [str(root)]
401+
if inputs and inputs.generator[0] != "-m":
402+
# Pre-package generators import validation and other sibling modules.
403+
import_paths.insert(0, str(Path(inputs.generator[0]).resolve().parent))
404+
env = os.environ.copy()
405+
if env.get("PYTHONPATH"):
406+
import_paths.append(env["PYTHONPATH"])
407+
env["PYTHONPATH"] = os.pathsep.join(import_paths)
408+
if inputs and inputs.root:
409+
env["INFERENCEX_REPOSITORY_ROOT"] = str(root)
399410
try:
400411
result = subprocess.run(
401-
command,
402-
capture_output=True,
403-
text=True,
404-
check=True,
405-
cwd=inputs.root if inputs else None,
406-
env={**os.environ, "INFERENCEX_REPOSITORY_ROOT": str(Path(inputs.root).resolve())}
407-
if inputs and inputs.root
408-
else None,
412+
command, capture_output=True, text=True, check=True, cwd=root, env=env
409413
)
410414
except subprocess.CalledProcessError as exc:
411415
print(exc.stderr)

0 commit comments

Comments
 (0)