Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6798d87
[TRTLLM-12838][infra] CBTS: coverage-based test selection (Tier 2 sel…
crazydemo Jul 27, 2026
8a9ea56
[TRTLLM-12838][infra] CBTS: key coverage selection on stage family
crazydemo Aug 4, 2026
170b2de
[TRTLLM-12838][infra] CBTS: treat Ray stages as untrusted coverage
crazydemo Aug 4, 2026
f65363c
[TRTLLM-12838][infra] CBTS: record which coverage DB build a decision…
crazydemo Aug 4, 2026
3edc41a
[TRTLLM-12838][infra] CBTS: align coverage_explain with selector safe…
crazydemo Aug 4, 2026
8db2bd2
[TRTLLM-12838][infra] CBTS: drop unused RuleResult.detail
crazydemo Aug 4, 2026
885d3e4
[TRTLLM-12838][infra] CBTS: fix import order in the coverage selectio…
crazydemo Aug 4, 2026
f6945f1
[TRTLLM-12838][infra] CBTS: always run CPU stages, and fix their gpu-…
crazydemo Aug 6, 2026
45cd2c2
[TRTLLM-12838][infra] CBTS: bound import-executed changes by file, or…
crazydemo Aug 6, 2026
35f9dd3
[TRTLLM-12838][infra] CBTS: select the coverage DB by revision, and r…
crazydemo Aug 6, 2026
53b9fea
[TRTLLM-12838][infra] CBTS: fetch core-file diffs, and forward enable…
crazydemo Aug 6, 2026
a9f5a9b
[TRTLLM-12838][infra] CBTS: bound files whose patch the API omitted, …
crazydemo Aug 6, 2026
d5f95f2
[TRTLLM-12838][infra] CBTS: use the DB's own completeness signal, and…
crazydemo Aug 6, 2026
da7a44d
[TRTLLM-12838][infra] CBTS: bound closure changes the same way as imp…
crazydemo Aug 6, 2026
8c8f0fd
[TRTLLM-12838][infra] CBTS: condense the coverage-selection comments …
crazydemo Aug 6, 2026
c6f1279
[TRTLLM-12838][infra] CBTS: document the coverage tier and refresh th…
crazydemo Aug 6, 2026
6e637d4
[TRTLLM-12838][infra] CBTS: measure coverage-DB lag via the forge com…
crazydemo Aug 7, 2026
58c2759
[TRTLLM-12838][infra] CBTS: make the compare API authoritative for th…
crazydemo Aug 7, 2026
ed2cf98
[TRTLLM-12838][infra] CBTS: drop the local-git path for the coverage-…
crazydemo Aug 7, 2026
8741f8a
[TRTLLM-12838][infra] CBTS: drop artifact.py's unused download entry …
crazydemo Aug 7, 2026
f4b587d
[TRTLLM-12838][infra] CBTS: condense artifact.py's header and the aud…
crazydemo Aug 7, 2026
66dae86
[TRTLLM-12838][infra] CBTS: say the audit lag is measured against main
crazydemo Aug 7, 2026
8e2eb7f
[TRTLLM-12838][infra] CBTS: gate the coverage tier on the touch DB's …
crazydemo Aug 7, 2026
ea65c2b
[TRTLLM-12838][infra] CBTS: fail closed on import-executed changes
crazydemo Aug 7, 2026
36ecd14
[TRTLLM-12838][infra] CBTS: a comment-only diff impacts nothing
crazydemo Aug 7, 2026
dd8f518
[TRTLLM-12838][infra] CBTS: keep the freshness gate out of dry-run re…
crazydemo Aug 7, 2026
ac8a601
[TRTLLM-12838][infra] CBTS: point COLLECTION.md at what the import ga…
crazydemo Aug 7, 2026
3773747
[TRTLLM-12838][infra] CBTS: gate coverage freshness on the PR's base,…
crazydemo Aug 11, 2026
915f085
[TRTLLM-12838][infra] CBTS: pass the coverage DB's selection JSON thr…
crazydemo Aug 11, 2026
dc0e287
[TRTLLM-12838][infra] CBTS: fetch the coverage DB in Python, not in t…
crazydemo Aug 11, 2026
d203a28
[TRTLLM-12838][infra] CBTS: hand back artifact.py's paths instead of …
crazydemo Aug 11, 2026
dac0c9d
[TRTLLM-12838][infra] CBTS: drop the import-section break a stray loc…
crazydemo Aug 11, 2026
4a9cbe3
drift tolerance change to 10
crazydemo Aug 18, 2026
a8032b5
[TRTLLM-12838][infra] Move CBTS filtering out of launchTestJobs
crazydemo Aug 18, 2026
03a7302
[TRTLLM-12838][infra] CBTS: disable coverage tier by default
crazydemo Aug 19, 2026
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
67 changes: 45 additions & 22 deletions jenkins/L0_MergeRequest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def DISABLE_CBTS = "disable_cbts"
// Kill switch for CBTS per-test coverage; official post-merge pipeline only, single-GPU stages only in Phase 1.
@Field
def ENABLE_CBTS_COVERAGE = true
// Rollout switch for pre-merge Tier 2 coverage-based narrowing. Keep collection
// enabled above while this remains off so a later pilot allowlist has fresh data.
@Field
def ENABLE_CBTS_COVERAGE_TIER = false

def testFilter = [
(REUSE_TEST): gitlabParamsFromBot.get(REUSE_TEST, null),
Expand Down Expand Up @@ -846,8 +850,9 @@ def getCbtsResult(pipeline, testFilter, globalVars)
// pyyaml is needed by main.py's blocks.py to parse test-db YAMLs.
sh "apt-get update -qq && apt-get install -y -qq python3-yaml"

// Shadow audit: download the latest merged touch DB and log its health + HEAD coverage gap (diagnostic only).
_cbtsCoverageAudit(pipeline)
// Download the touch DB only when Tier 2 is enabled. Tier 1 rules still
// run while the coverage tier is disabled during the initial rollout.
def coverageDb = _cbtsCoverageDb(pipeline)

// Ask Python which file patterns need diffs, fetch them.
def patternsOut = sh(
Expand All @@ -872,10 +877,11 @@ def getCbtsResult(pipeline, testFilter, globalVars)
def inputPath = "${LLM_ROOT}/cbts_input.json"
writeFile file: inputPath, text: inputJson

def output = sh(
script: "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/main.py cbts_input.json",
returnStdout: true,
)
def mainCmd = "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/main.py cbts_input.json"
if (coverageDb) {
mainCmd += " --coverage-db ${coverageDb.path} --coverage-db-meta ${coverageDb.meta}"
}
def output = sh(script: mainCmd, returnStdout: true)

def result = _cbtsParseSelectionResult(output)
if (result.scope == null) {
Expand Down Expand Up @@ -917,30 +923,45 @@ def getCbtsResult(pipeline, testFilter, globalVars)
}
}

// Download the latest merged touch DB and run coverage_audit.py on it; best-effort, never changes the CBTS decision.
// Resolve the optional Tier 2 input behind an explicit rollout gate. Keeping
// this separate makes the follow-up pilot allowlist a small policy change.
def _cbtsCoverageDb(pipeline)
{
if (!ENABLE_CBTS_COVERAGE_TIER) {
pipeline.echo("CBTS: coverage tier disabled — running Tier 1 only")
return null
}
return _cbtsCoverageAudit(pipeline)
}

// Fetch the touch DB and audit it; artifact.py's {path, meta} verbatim, or null on failure.
def _cbtsCoverageAudit(pipeline)
{
try {
def covDir = "${LLM_ROOT}/cbts_cov"
def url = sh(
script: "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/coverage_selection/artifact.py --print-url || true",
returnStdout: true,
).trim()
if (!url) {
pipeline.echo("CBTS audit: no coverage DB artifact found — skipping")
return
// artifact.py resolves, downloads and unpacks; paths come back
// ${LLM_ROOT}-relative, matching the main.py caller's `cd ${LLM_ROOT}`.
// The checked-out revision is the PR head; its merge base is what drift is measured against.
def prHead = env.gitlabMergeRequestLastCommit ?: ""
def readyJson = ""
withCredentials([usernamePassword(credentialsId: 'github-cred-trtllm-ci', usernameVariable: 'NOT_USED_YET', passwordVariable: 'GITHUB_API_TOKEN')]) {
readyJson = sh(
script: "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/coverage_selection/artifact.py " +
"--prepare cbts_cov${prHead ? " --pr-head ${prHead}" : ""} || true",
returnStdout: true,
).trim()
}
sh "mkdir -p ${covDir}"
// wget the tarball (retrying) and extract the sqlite.
trtllm_utils.llmExecStepWithRetry(pipeline, script:
"wget -nv '${url}' -O ${covDir}/cbts_pystart_report.tar.gz && " +
"tar xzf ${covDir}/cbts_pystart_report.tar.gz -C ${covDir}")
sh "python3 ${LLM_ROOT}/jenkins/scripts/cbts/tools/coverage_audit.py " +
"--db ${covDir}/cbts_touchmap.sqlite"
if (!readyJson) {
pipeline.echo("CBTS audit: no coverage DB could be prepared — skipping Tier 2")
return null
}
def ready = new groovy.json.JsonSlurper().parseText(readyJson)
sh "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/tools/coverage_audit.py --db ${ready.path}"
return ready
} catch (InterruptedException e) {
throw e
} catch (Exception e) {
pipeline.echo("CBTS audit: skipped (non-fatal): ${e.message}")
return null
}
}

Expand Down Expand Up @@ -1033,6 +1054,8 @@ def _cbtsParseSelectionResult(String text)
// Explicit null check preserves `false`; default True is safe.
sanity_required: data.sanity_required != null ? data.sanity_required : true,
perfsanity_required: data.perfsanity_required != null ? data.perfsanity_required : true,
// Coverage tier omits multi-GPU stages; L0_Test re-adds them under this flag.
enable_multi_gpu: data.enable_multi_gpu ?: false,
]
}

Expand Down
87 changes: 52 additions & 35 deletions jenkins/L0_Test.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2549,6 +2549,56 @@ def cbtsResizeSplits(configs) {
return resized
}

// CBTS Layer 2: replace the normal stage set with the selector's affected
// stages while retaining the baseline sanity and multi-GPU gates.
def filterCbtsStageJobs(parallelJobs, parallelJobsFiltered, multiGpuJobs, testFilter) {
def cbts = testFilter[(CBTS_RESULT)]
if (cbts == null) {
return parallelJobsFiltered
}

// cbtsResizeSplits renames only narrowed stages (those in
// affected_stage_split_counts) to `-cbts`; affected-but-not-narrowed
// stages keep their original name, so match each per its actual key.
def stageSuffix = cbts.cbts_test_db_artifact_path ? CBTS_STAGE_SUFFIX : ""
def narrowed = (cbts.affected_stage_split_counts ?: [:]).keySet()
def affectedSet = (cbts.affected_stages ?: []).collect {
(stageSuffix && narrowed.contains(it)) ? (it + stageSuffix) : it
} as Set
def needsSanity = cbts.sanity_required
def needsPerfSanity = cbts.perfsanity_required
def filtered = parallelJobs.findAll { key, _ ->
if (key.contains("-OnDemand-")) {
return false
}
if (key =~ /Post-Merge/) return affectedSet.contains(key)
return affectedSet.contains(key) ||
(needsSanity && key =~ /PackageSanityCheck/) ||
(needsPerfSanity && key =~ /PerfSanity/)
}
if (affectedSet.isEmpty()) {
if (filtered.isEmpty()) {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch + nothing force-kept → no-op"
} else {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch — running " +
"${filtered.size()} force-kept stage(s) only"
}
} else if (filtered) {
echo "CBTS [${cbts.scope}]: limiting to ${filtered.size()} stages " +
"(sanity_required=${needsSanity}, perfsanity_required=${needsPerfSanity})"
} else {
echo "CBTS [${cbts.scope}]: empty stage set after filtering"
}

// The coverage tier omits multi-GPU; re-add it under the baseline gate.
if (cbts.enable_multi_gpu && testFilter[(MULTI_GPU_FILE_CHANGED)]) {
filtered += multiGpuJobs
echo "CBTS [${cbts.scope}]: multi-GPU file changed → running " +
"${multiGpuJobs.size()} multi-GPU stage(s) at baseline"
}
return filtered
}

// True when an exception indicates the K8s dispatcher pod this SLURM stage runs
// inside died mid-run -- kubelet eviction, container termination, or the JNLP
// agent otherwise going offline. Retrying inside such a pod is futile (every
Expand Down Expand Up @@ -6539,41 +6589,8 @@ def launchTestJobs(pipeline, testFilter, globalVars)
checkStageNameSet(testFilter[(EXTRA_STAGE_LIST)], fullSet, EXTRA_STAGE_LIST)
}

// CBTS Layer 2: replace `parallelJobsFiltered` with affected stages plus
// PackageSanityCheck (kept iff sanity_required) and PerfSanity (kept iff
// perfsanity_required). Pure -Perf- stages run only when CBTS selects them
// (present in affected_stages). Post-Merge stages are never force-kept;
// they only run when explicitly listed in affected_stages.
def cbts = testFilter[(CBTS_RESULT)]
if (cbts != null) {
// Match the -cbts rename cbtsResizeSplits applies to narrowed stages.
def stageSuffix = cbts.cbts_test_db_artifact_path ? CBTS_STAGE_SUFFIX : ""
def affectedSet = (cbts.affected_stages ?: []).collect { it + stageSuffix } as Set
def needsSanity = cbts.sanity_required
def needsPerfSanity = cbts.perfsanity_required
parallelJobsFiltered = parallelJobs.findAll { key, _ ->
if (key.contains("-OnDemand-")) {
return false
}
if (key =~ /Post-Merge/) return affectedSet.contains(key)
return affectedSet.contains(key) ||
(needsSanity && key =~ /PackageSanityCheck/) ||
(needsPerfSanity && key =~ /PerfSanity/)
}
if (affectedSet.isEmpty()) {
if (parallelJobsFiltered.isEmpty()) {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch + nothing force-kept → no-op"
} else {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch — running " +
"${parallelJobsFiltered.size()} force-kept stage(s) only"
}
} else if (parallelJobsFiltered) {
echo "CBTS [${cbts.scope}]: limiting to ${parallelJobsFiltered.size()} stages " +
"(sanity_required=${needsSanity}, perfsanity_required=${needsPerfSanity})"
} else {
echo "CBTS [${cbts.scope}]: empty stage set after filtering"
}
}
parallelJobsFiltered = filterCbtsStageJobs(
parallelJobs, parallelJobsFiltered, multiGpuJobs, testFilter)

if (globalVars[RUN_MODE] == "nightly_release") {
parallelJobsFiltered = sanityCheckJobs
Expand Down
28 changes: 28 additions & 0 deletions jenkins/scripts/cbts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ run, based on what the PR changed. New rules are added in Python only.

---

## Selection tiers

| Tier | Where | Basis |
|---|---|---|
| **1. Rules** | `rules/` | Path patterns and diffs. Each rule claims the files it understands and narrows to the blocks they affect. |
| **2. Coverage** | `coverage_selection/`, `coverage_tier.py` | Runs only on what Tier 1 left unclaimed (the *residual*), and only when every residual file is core Python present in the touch DB. Maps changed lines to qualnames and removes entries no changed function reaches. Declines to the full run otherwise. See `coverage_selection/SELECTION.md`. |

The touch DB is produced by the post-merge collection under `coverage_utils/`;
see `coverage_utils/COLLECTION.md` for what it does and does not record.

## Consumption layers

CBTS narrows test cases only; Build always runs.
Expand Down Expand Up @@ -75,8 +85,18 @@ jenkins/scripts/cbts/
│ ├── visual_gen_rule.py
│ ├── spec_dec_rule.py
│ └── out_of_scope_rule.py
├── coverage_tier.py Tier 2 entry: applies the selector to the test-db YAMLs, classifies every candidate entry
├── coverage_selection/
│ ├── SELECTION.md how a decision is made: qualname concepts, decline gates, narrowing
│ ├── selector.py CoverageSelector.decide(): changed lines → qualnames → impacted / skippable per stage family
│ ├── qualname_map.py changed lines → co_qualname, plus the import-time and closure classifications
│ ├── touch_db.py read-only accessor over cbts_touchmap.sqlite + the untrusted-capture signals
│ └── artifact.py resolve which post-merge touch DB to use (by collected revision)
├── coverage_utils/ post-merge collection that produces the touch DB (see its README / COLLECTION.md)
└── tools/
├── dryrun.py replay CBTS over historical commits → per-PR summary.txt + filtered YAMLs + INDEX.md (debug only)
├── coverage_audit.py report a touch DB's format, scale, untrusted rate and HEAD coverage gap
├── coverage_explain.py explain one commit's decision case by case (delegates to CoverageSelector)
└── report_cbts_decision.py post the decision (hit-stage count, case-level skip rate, fallback) to OpenSearch for CI-health monitoring
```

Expand Down Expand Up @@ -294,6 +314,14 @@ CBTS defers to the existing filter chain when:
but cannot decide; e.g. testdef blast-radius cap, testlist structural
YAML edit)
- Combined scope is `None` (incompatible mix)
- Tier 2 declines: a residual file is not core Python, is absent from the
touch DB, has an import-executed change (module / class body, signature or
decorator line), has no usable patch, has unparsable source, or has a closure
change with no wider row set (see `coverage_selection/SELECTION.md` §3-4)
- No touch DB artifact could be resolved — Tier 2 never runs
- The resolved DB sits more than `--coverage-max-drift` commits from the PR's
base commit, on either side, or an unmeasurable distance from it — Tier 2
declines (`coverage_freshness` = `stale` / `unknown`)
- Layer 3 narrowing would empty a block — block keeps original tests
- `cbts_test_db` tarball upload or download/extraction fails — renderTestDB falls back to source
- Narrowed YAML missing/empty on a stage agent — renderTestDB falls back
Expand Down
10 changes: 10 additions & 0 deletions jenkins/scripts/cbts/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import json
import math
import re
import shutil
from dataclasses import dataclass, field
from fnmatch import fnmatch
from pathlib import Path
Expand Down Expand Up @@ -433,6 +434,9 @@ def _classify_map_var(var_name: str) -> Optional[str]:
return None


# Stages CBTS always runs, whatever the decision.
ALWAYS_RUN_STAGE_PREFIX = "CPU-"

# Backend name -> mako value. Same patterns as getMakoArgsFromStageName in
# jenkins/L0_Test.groovy (line ~2079). IMPORTANT: keep this list in sync.
_BACKEND_PATTERNS = [
Expand All @@ -441,6 +445,7 @@ def _classify_map_var(var_name: str) -> Optional[str]:
("-Triton-", "triton"),
("-FMHA-", "fmha"),
("-AutoDeploy-", "autodeploy"),
("-Generic-", "generic"),
("-Verl-", "verl"),
]

Expand Down Expand Up @@ -488,6 +493,9 @@ def derive_mako_from_stage(stage_name: str) -> dict[str, str]:
mako["gpu"] = gpu_match.group(1).lower()
count_match = _GPU_COUNT_RE.search(stage_name)
mako["system_gpu_count"] = count_match.group(1) if count_match else "1"
# renderTestDB hardcodes system_gpu_count=0 for CPU- stages (no N_GPUs token).
if stage_name.startswith("CPU-"):
mako["system_gpu_count"] = "0"

return mako

Expand Down Expand Up @@ -749,6 +757,8 @@ def write_filtered_test_db(
tests are kept (prevents silent skip from typo'd waive ids or granularity
mismatch). The block itself is still kept either way.
"""
# Clear first: a leftover YAML from an earlier run would ship in the artifact.
shutil.rmtree(output_dir, ignore_errors=True)
output_dir.mkdir(parents=True, exist_ok=True)

affected_stems = {stem for stem, _ in block_filters}
Expand Down
Loading
Loading