Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jenkins/scripts/cbts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Nine rules, registered in `cbts/command/main.py::RULE_CLASSES`:
| `WaivesRule` | `waiveonly` | `tests/integration/test_lists/waives.txt` |
| `TestsDefRule` | `testdefonly` | `tests/**/*` (.py via AST; data files via dir walk-up) |
| `TestListRule` | `testlistonly` | `tests/integration/test_lists/test-db/*.yml` |
| `VisualGenRule` | `visualgenonly` | `examples/visual_gen/**`, `scripts/visualgen_eval/**`, `tensorrt_llm/_torch/visual_gen/**`, `tensorrt_llm/media/**`, `tensorrt_llm/visual_gen/**` (excl. docs; reference images such as `cat_piano.png` ARE test fixtures and stay claimed; outward-facing files force fallback) |
| `VisualGenRule` | `visualgenonly` | VG source and the exact VG serve schema; public VG/media changes also keep full CPU and shared serving tests, package sanity, and performance sanity; missing required validation forces fallback |
| `SpecDecRule` | `specdeconly` | `tensorrt_llm/_torch/speculative/**`, `tensorrt_llm/models/{eagle,medusa,redrafter}/**`, `examples/{eagle,medusa,redrafter,draft_target_model,ngram}/**`, `examples/llm-api/llm_speculative_decoding.py` (excl. docs; other suffixes incl. images kept as potential test fixtures) |
| `ModelingV2Rule` | `modelingv2only` | `tensorrt_llm/_torch/_experimental/modeling_v2/**`, `cpp/tensorrt_llm/modeling_v2/**`, `cpp/cmake/modules/modeling_v2.cmake` (excl. docs); per model node for changes under `models/` |
| `AgentFlowRule` | `agentflowonly` | `agent-flow/**` (excl. docs and out-of-scope metadata) |
Expand All @@ -65,7 +65,7 @@ See `cbts/rules/README.md` for per-rule logic.
| `waiveonly` | `WaivesRule` fired solo: PR only edits `waives.txt`. |
| `testdefonly` | `TestsDefRule` fired solo: PR only edits files under `tests/**/*`. |
| `testlistonly` | `TestListRule` fired solo: PR only adds entries under `tests/integration/test_lists/test-db/*.yml`. |
| `visualgenonly` | `VisualGenRule` fired solo: PR only touches VisualGen internal source paths (`examples/visual_gen/**`, `scripts/visualgen_eval/**`, `tensorrt_llm/_torch/visual_gen/**`; excl. docs; image fixtures like `cat_piano.png` are claimed). Narrows to blocks containing VG test entries. Outward-facing files under `tensorrt_llm/visual_gen/**` and `tensorrt_llm/media/**` (eagerly imported by `trtllm-serve`) force `null` fallback. |
| `visualgenonly` | `VisualGenRule` fired solo. Internal VG changes select VG test entries; schema changes also retain schema validation. Public VG/media changes additionally retain the complete CPU suite, shared serving integration tests, package sanity, and performance sanity. Missing required validation forces `null` fallback. |
| `specdeconly` | `SpecDecRule` fired solo: PR only touches speculative-decoding source paths (`tensorrt_llm/_torch/speculative/**`, `tensorrt_llm/models/{eagle,medusa,redrafter}/**`, `examples/{eagle,medusa,redrafter,draft_target_model,ngram}/**`, `examples/llm-api/llm_speculative_decoding.py`; excl. docs). Narrows to blocks containing spec-dec test entries (eagle / medusa / redrafter / ngram / draft-target-model / MTP). |
| `modelingv2only` | `ModelingV2Rule` fired solo: PR only touches modeling_v2 code -- the subtree (`tensorrt_llm/_torch/_experimental/modeling_v2/**`, each family's native code in its `csrc/` included: one torch-op library per model family) or what the family libraries share (`cpp/tensorrt_llm/modeling_v2/**`, `cpp/cmake/modules/modeling_v2.cmake`); excl. docs, which are a fifth of the Python subtree — every catalog entry carries a contract document. A change under `models/` selects its model node's unit tests and target accuracy gates, plus whole-tree checks; shared code or an unreadable routing tree selects every modeling_v2 entry. No outward-facing fallback is needed: nothing beyond the resolver module is imported, and no family library is loaded, unless `LLM(modeling_v2=...)` is on; the CPU checks that keep this true run in every scope. The callers outside the subtree (`_torch/pyexecutor/model_loader.py`, `_torch/models/modeling_auto.py`) are left unclaimed, so touching them falls back to baseline. |
| `agentflowonly` | `AgentFlowRule` fired solo: PR only touches `agent-flow/**` source or test files (excl. docs). Runs `CPU-AgentFlow-UnitTest`. |
Expand Down
16 changes: 15 additions & 1 deletion jenkins/scripts/cbts/cbts/command/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import json
import sys
from collections import Counter
from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Optional
Expand Down Expand Up @@ -64,6 +65,7 @@
from cbts.rules.modeling_v2 import ModelingV2Rule
from cbts.rules.openengine import OpenEngineRule
from cbts.rules.out_of_scope import OutOfScopeRule, is_out_of_scope
from cbts.rules.python_formatting import PythonFormattingRule, is_formatting_only_change
from cbts.rules.spec_dec import SpecDecRule
from cbts.rules.test_list import TestListRule
from cbts.rules.tests_def import TestsDefRule
Expand All @@ -77,6 +79,7 @@

# Classes are used for `--list-needed-diffs` (no need to construct).
RULE_CLASSES: list[type[Rule]] = [
PythonFormattingRule,
WaivesRule,
TestsDefRule,
TestListRule,
Expand All @@ -96,6 +99,7 @@ def build_rules(
repo_root: Path,
) -> list[Rule]:
return [
PythonFormattingRule(),
WaivesRule(yaml_index, stages),
TestsDefRule(yaml_index, stages, repo_root=repo_root),
TestListRule(yaml_index, stages, repo_root=repo_root),
Expand Down Expand Up @@ -274,7 +278,9 @@ def run(self, pr: PRInputs, rules: list[Rule]) -> SelectionResult:
code_pr = replace(
pr,
changed_files=[
path for path in pr.changed_files if path not in pr.docstring_only_files
path
for path in pr.changed_files
if path not in pr.docstring_only_files | pr.formatting_only_files
],
)
pairs: list[tuple[Rule, RuleResult]] = []
Expand Down Expand Up @@ -365,16 +371,23 @@ def _load_pr_inputs(input_json_path: Path, repo_root: Path) -> PRInputs:
# A null diff (PR API omits the patch for binary, rename, or
# too-large diffs) normalizes to an empty diff.
raw_diffs = {path: d or "" for path, d in data.get("diffs", {}).items()}
raw_diff_counts = Counter(raw_diffs.values())
diffs = {path: strip_noop_diff_lines(diff) for path, diff in raw_diffs.items()}
docstring_files: set[str] = set()
docstring_only_files: set[str] = set()
formatting_only_files: set[str] = set()
for path, raw_diff in raw_diffs.items():
if not path.endswith(".py") or not raw_diff or is_out_of_scope(path):
continue
try:
source = (repo_root / path).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
# Forge APIs repeat a renamed file's patch for its old and new paths.
# Content equivalence alone cannot prove that such a move has no impact.
if raw_diff_counts[raw_diff] == 1 and is_formatting_only_change(source, raw_diff):
formatting_only_files.add(path)
continue
executable, changed = split_docstring_diff(source, raw_diff)
if changed:
docstring_files.add(path)
Expand All @@ -388,6 +401,7 @@ def _load_pr_inputs(input_json_path: Path, repo_root: Path) -> PRInputs:
raw_diffs=raw_diffs,
docstring_files=docstring_files,
docstring_only_files=docstring_only_files,
formatting_only_files=formatting_only_files,
)


Expand Down
56 changes: 41 additions & 15 deletions jenkins/scripts/cbts/cbts/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,31 @@ for the overall CBTS architecture.

| File | Class | Scope | Triggers on |
|---|---|---|---|
| `python_formatting.py` | `PythonFormattingRule` | `noop` | Python files with verified equivalent complete ASTs and unchanged comments |
| `waives.py` | `WaivesRule` | `waiveonly` | `tests/integration/test_lists/waives.txt` |
| `tests_def.py` | `TestsDefRule` | `testdefonly` | `tests/**/*` (any file under tests/) |
| `test_list.py` | `TestListRule` | `testlistonly` | `tests/integration/test_lists/test-db/*.yml` |
| `visual_gen.py` | `VisualGenRule` | `visualgenonly` | `examples/visual_gen/**`, `scripts/visualgen_eval/**`, `tensorrt_llm/_torch/visual_gen/**`, `tensorrt_llm/media/**`, `tensorrt_llm/visual_gen/**` (each excl. docs) |
| `visual_gen.py` | `VisualGenRule` | `visualgenonly` | `examples/visual_gen/**`, `scripts/visualgen_eval/**`, `tensorrt_llm/_torch/visual_gen/**`, `tensorrt_llm/media/**`, `tensorrt_llm/visual_gen/**` (each excl. docs), and the exact VisualGen serve schema |
| `spec_dec.py` | `SpecDecRule` | `specdeconly` | `tensorrt_llm/_torch/speculative/**`, `tensorrt_llm/models/{eagle,medusa,redrafter}/**`, `examples/{eagle,medusa,redrafter,draft_target_model,ngram}/**`, `examples/llm-api/llm_speculative_decoding.py` (each excl. docs) |
| `modeling_v2.py` | `ModelingV2Rule` | `modelingv2only` | `tensorrt_llm/_torch/_experimental/modeling_v2/**`, `cpp/tensorrt_llm/modeling_v2/**`, `cpp/cmake/modules/modeling_v2.cmake` (excl. docs); per model node for changes under `models/` |
| `agent_flow.py` | `AgentFlowRule` | `agentflowonly` | `agent-flow/**` (excl. docs and out-of-scope metadata) → the single `CPU-AgentFlow-UnitTest` stage; not test-db-driven |
| `openengine.py` | `OpenEngineRule` | `openengineonly` | `tensorrt_llm/grpc/openengine/**` (excl. docs) → the `l0_cpu` block containing `unittest/grpc/openengine/` |
| `docs.py` | `DocsRule` | `docsonly` | Outside noop paths: `docs/**` and verified Python docstring edits → `CPU-Build_Docs` plus complete `l0_cpu`; other `**/*.md` / `**/*.rst` → `CPU-Build_Docs` only |
| `out_of_scope.py` | `OutOfScopeRule` | `noop` | Reviewed metadata paths, root issue-template and developer-assistant folders, `.gitignore` at any depth, conventional clang filenames under `cpp/`, verified static-check-only `pyproject.toml` edits, QA/dev lists, timing caches, microbenchmarks, and CBTS infrastructure |

## PythonFormattingRule

Claims Python files only after the raw patch matches the checked-out source
and both complete source images have identical ASTs, including docstrings and
type comments. Comment text must also match, preserving encoding and lint
directives. Missing or mismatched patches, syntax errors, empty pre-images,
and explicit rename/mode changes retain their normal impact. Repeated patches
for old/new rename paths are not classified as formatting.

Verified files are excluded from source rules and contribute `scope=noop`;
the entry point still retains the always-run CPU stages. A mixed PR continues
through the normal rules for every remaining changed file.

## DocsRule

Claims every file under `docs/` plus Markdown and reStructuredText files
Expand Down Expand Up @@ -203,8 +217,10 @@ Outcomes:

Path-only rule. Claims source files under `examples/visual_gen/`,
`scripts/visualgen_eval/`, `tensorrt_llm/_torch/visual_gen/`,
`tensorrt_llm/media/`, and `tensorrt_llm/visual_gen/` (excluding `.md`,
which `OutOfScopeRule` claims as noop). fused-DiT paths stay unclaimed:
`tensorrt_llm/media/`, and `tensorrt_llm/visual_gen/` (excluding documentation,
which `DocsRule` handles). It also claims the exact generated schema
`tensorrt_llm/schemas/trtllm-serve-visual-gen-config.schema.json`.
Fused-DiT paths stay unclaimed:
those diffs also touch `inplace_info()` in
`tensorrt_llm/_torch/compilation/utils.py`, which no VG rule claims.
Image suffixes are intentionally NOT excluded: VG ships reference images
Expand All @@ -222,16 +238,25 @@ live in `backend: pytorch` and `backend: tensorrt` blocks. A block

For each matched block, `block_filters` keeps only the VG entries.
Non-VG siblings in the same block stay governed by other rules.

Outward-facing fallback: VisualGen public symbols and
`tensorrt_llm.media.*` are imported eagerly by non-VG startup paths such
as `commands/serve.py`, `commands/utils.py`, and
`serve/openai_server.py`. Both package prefixes
(`tensorrt_llm/visual_gen/`, `tensorrt_llm/media/`) are listed in
`_VG_OUTWARD_PREFIXES`; touching any non-doc file under them claims the
changed files but emits `scope=None` so Selector falls back to baseline.
This protects trtllm-serve / trtllm-bench startup paths from VG
signature drift slipping through pre-merge.
Schema changes additionally keep the complete
`unittest/llmapi/test_trtllm_serve_config_schemas.py` validation entry (or
its enclosing test-directory entry). Missing validation stages force fallback.
Other generated schemas remain unclaimed.

Shared serving validation: changes under `tensorrt_llm/visual_gen/` or
`tensorrt_llm/media/` also keep every entry in `l0_cpu`, plus shared test
families under `test_e2e.py`, `examples/serve`, `disaggregated`, `llmapi`,
`unittest/llmapi`, and `unittest/disaggregated`. These cover public config,
protocol/media imports, normal and disaggregated serving, multimodal requests,
and online benchmarks. PackageSanityCheck and PerfSanity remain enabled.

The CPU suite must contain runnable API stability, serve-schema, lazy-server
import, and lazy-model-zoo validation. The serving chat/completions examples and
config-override precedence test must also resolve to stages scheduled in the trigger mode;
OnDemand stages and optional
pre-merge multi-GPU stages cannot satisfy this requirement. Missing or filtered
required validation keeps `scope=None`. Public changes therefore get a wider
test set than internal VG changes without requiring all unrelated GPU tests.

Outcomes:

Expand All @@ -242,8 +267,9 @@ Outcomes:
sanity off (VG changes don't affect wheel sanity); perfsanity on iff
a matched block lives in `l0_perf` or `*perf_sanity*`.
- VG source touched, any outward-facing path under
`tensorrt_llm/visual_gen/` or `tensorrt_llm/media/` → `scope=None`
(fallback).
`tensorrt_llm/visual_gen/` or `tensorrt_llm/media/` → `scope=visualgenonly`
with shared serving, full CPU, package sanity, and performance sanity.
Missing required shared validation → `scope=None` (fallback).
- VG source touched but no VG block found anywhere (defensive) →
`scope=None` (fallback).

Expand Down
3 changes: 2 additions & 1 deletion jenkins/scripts/cbts/cbts/rules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def format_reason(reason: object) -> str:

@dataclass
class PRInputs:
"""PR data, with executable selection diffs and verified docstring facts.
"""PR data, with executable diffs and verified docstring/formatting facts.

``raw_diffs`` preserves source reconstruction input. ``post_merge``
reflects /bot run --post-merge.
Expand All @@ -55,6 +55,7 @@ class PRInputs:
# Documentation contributions can coexist with executable edits in one file.
docstring_files: set[str] = field(default_factory=set)
docstring_only_files: set[str] = field(default_factory=set)
formatting_only_files: set[str] = field(default_factory=set)


@dataclass
Expand Down
87 changes: 87 additions & 0 deletions jenkins/scripts/cbts/cbts/rules/python_formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Recognize verified Python formatting changes without a coverage dependency."""

from __future__ import annotations

import ast
import io
import tokenize

from ._helpers import reconstruct_diff_pre_image
from .base import PRInputs, Rule, RuleResult


def is_formatting_only_change(post_source: str, diff: str) -> bool:
"""Compare complete syntax trees and comments after verifying the raw patch.

Keep docstrings, type comments, and comment directives in the comparison.
Missing source, malformed patches, and new or removed files remain unknown.
"""
if any(
line.startswith(
(
"new file mode ",
"deleted file mode ",
"old mode ",
"new mode ",
"rename from ",
"rename to ",
"copy from ",
"copy to ",
)
)
for line in diff.splitlines()
):
return False
pre_source = reconstruct_diff_pre_image(post_source, diff)
if pre_source is None:
return False
try:
before = ast.parse(pre_source, type_comments=True)
after = ast.parse(post_source, type_comments=True)
comments = [
[
token.string
for token in tokenize.generate_tokens(io.StringIO(source).readline)
if token.type == tokenize.COMMENT
]
for source in (pre_source, post_source)
]
except (SyntaxError, ValueError, tokenize.TokenError):
return False
return (
bool(before.body)
and ast.dump(before, include_attributes=False) == ast.dump(after, include_attributes=False)
and comments[0] == comments[1]
)


class PythonFormattingRule(Rule):
name = "pythonformatting"
needs_diff_for = ("**/*.py",)
handles_docstrings = True

def apply(self, pr: PRInputs) -> RuleResult | None:
if not pr.formatting_only_files:
return None
return RuleResult(
handled_files=set(pr.formatting_only_files),
affected_stages=set(),
scope="noop",
reason=f"pythonformatting: {len(pr.formatting_only_files)} verified formatting-only file(s)",
sanity_relevant=False,
perfsanity_relevant=False,
)
Loading
Loading