feat(cccl): integrate missing CCCL directories — python/, ci/, .agent/, docs/, test/

Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream:

Added:
- python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms
  Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc.
  Includes 204 .py files with full test coverage for all 27 algorithms
- ci/ (163 files) — Build/test infrastructure
  build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml
  Directly maps to our [INFRA-CI] and [INFRA-BUILD] items
- .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL
  cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md
- docs/ (491 files) — Official CCCL documentation
  CI references, CMake guides, Python compute docs, libcudacxx PTX docs
- test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar)
- Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml
- CLAUDE.md symlink → AGENTS.md (NVIDIA's standard)

cccl_upstream now mirrors full NVIDIA/cccl structure:
  Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks)
  After:  53M (+python +ci +docs +.agent +test +configs)

This completes the CCCL base needed for:
- [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds
- [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations
- [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh
- Agent workflow: .agent/skills/ for consistent style and test patterns
This commit is contained in:
muh-bot
2026-08-07 02:34:33 +00:00
parent 3f97dca7ad
commit 2a7ca101d7
908 changed files with 121615 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
# Compile-time benchmark CI contracts
The compile-time benchmark CI flow is configured from `ci/matrix.yaml` under
`compile_time.pull_request`.
## Matrix schema
Each config is a GitHub Actions matrix entry:
```yaml
compile_time:
pull_request:
- id: public-headers-gcc13
name: Public headers compile-time bench
gpu: rtx2080
launch_args: "--cuda 13.3 --host gcc13"
baseline_ref: origin/main
preset: all-dev
targets:
- cub.headers.base
args: "-arch native"
slices:
- id: total-compilation
title: TU total compilation
filter: total-compilation
timing: inclusive
sort: total
top: 15
threshold: 0.001
```
Required config fields are `id`, `name`, `gpu`, `launch_args`,
`baseline_ref`, `preset`, `targets`, and `slices`. `args`, `comment`, and
`artifact_retention_days` are optional.
Required slice fields are `id`, `title`, `filter`, `timing`, `sort`, `top`, and
`threshold`. Slice `children` may be used to group nested report sections in the
PR comment. Empty slice sections are omitted recursively by the renderer unless
the summary manifest carries warnings for that slice.
`ci/compile_time/parse_matrix.py ci/matrix.yaml --workflow pull_request` emits
the GitHub Actions matrix JSON. Missing or empty `compile_time.pull_request`
emits `{"include":[]}`.
In baseline comparisons, `threshold` is measured against the total selected
inclusive/exclusive impact across all matched traces. The per-side reports still
use `sort` for their own top-N ordering; comparison worse/better tables always
rank by total impact so a change repeated across many traces is not hidden by a
larger single-trace movement.
## Report contract
`summarize_events.py --slices <json>` writes per-slice CSVs under
`event_reports/<slice-id>/` and writes a normalized `event_reports/summary.json`
manifest. The manifest is the renderer contract; CSVs are human artifacts.
Configured slices that match no events, have no matching trace files, or have no
comparable event keys record warnings in the manifest so reporting failures are
not presented as ordinary no-regression results.
In comparison mode, the wrapper preserves:
- current raw traces: `compile_time/raw_traces`
- baseline raw traces: `compile_time/baseline_raw_traces`
- Perfetto copies: `compile_time/perfetto_traces/current` and
`compile_time/perfetto_traces/baseline`
## PR comments
`render_pr_comment.py` reads `summary.json`, config metadata, and an artifacts
URL, then writes the sticky PR comment body. Regressions and improvements are
rendered in separate `<details>` blocks and are never mixed in one table.
Warnings are rendered separately and keep their slice visible even when there
are no regression/improvement rows.
The reusable workflow uses the sticky-comment header
`compile-time-bench-<config-id>` with `hide_and_recreate: true`, so previous
comments for the same config are archived as outdated.

View File

@@ -0,0 +1,333 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "903704f7",
"metadata": {},
"source": [
"# Compile-Time Analytics\n",
"\n",
"This notebook helps you:\n",
"\n",
"1. Run `ci/build_compile_time_bench.sh` from the repo root.\n",
"2. Load and inspect an all-header processing CSV.\n",
"3. Explore high-impact headers by TU coverage and average processing time.\n",
"4. Build combined ranking scores to identify optimization targets.\n",
"\n",
"Expected CSV columns include:\n",
"- `header_path`\n",
"- `include_tu_count`\n",
"- `avg_process_time_s`\n",
"- `total_process_time_s`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ad58db28",
"metadata": {},
"outputs": [],
"source": [
"%pip install pandas matplotlib plotly nbformat"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16ba6808",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import shlex\n",
"import sys\n",
"from pathlib import Path\n",
"\n",
"import pandas as pd\n",
"\n",
"pd.set_option(\"display.max_colwidth\", 160)\n",
"pd.set_option(\"display.width\", 200)\n",
"pd.set_option(\"display.max_columns\", 20)\n",
"\n",
"REPO_ROOT = Path.cwd()\n",
"while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n",
" REPO_ROOT = REPO_ROOT.parent\n",
"\n",
"if not (REPO_ROOT / \".git\").exists():\n",
" raise RuntimeError(\n",
" \"Could not locate repo root (.git). Start notebook from inside the CCCL repo.\"\n",
" )\n",
"\n",
"print(f\"Repo root: {REPO_ROOT}\")\n",
"print(f\"Python executable: {sys.executable}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "373652c8",
"metadata": {},
"outputs": [],
"source": [
"# --- Run build_compile_time_bench.sh (file-processing mode) ---\n",
"import subprocess\n",
"from pathlib import Path\n",
"\n",
"if \"REPO_ROOT\" not in globals():\n",
" REPO_ROOT = Path.cwd()\n",
" while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n",
" REPO_ROOT = REPO_ROOT.parent\n",
"\n",
"output_csv = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n",
"cmd = [\n",
" \"bash\",\n",
" str(REPO_ROOT / \"ci\" / \"build_compile_time_bench.sh\"),\n",
" *shlex.split(os.environ.get(\"COMPILE_TIME_BUILD_ARGS\", \"\")),\n",
" \"--\",\n",
" \"-f\",\n",
" \"file-processing\",\n",
" \"-e\",\n",
" \"-n\",\n",
" os.environ.get(\"COMPILE_TIME_TOP_N\", \"5000\"),\n",
" \"--sort\",\n",
" \"total\",\n",
" \"--output-csv\",\n",
" str(output_csv),\n",
"]\n",
"\n",
"print(\"Command:\")\n",
"print(\" \" + \" \".join(shlex.quote(x) for x in cmd))\n",
"\n",
"subprocess.run(cmd, cwd=REPO_ROOT, check=True)\n",
"print(f\"\\nWrote: {output_csv}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12ad4c5c",
"metadata": {},
"outputs": [],
"source": [
"# --- Load CSV ---\n",
"csv_path = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n",
"if not csv_path.exists():\n",
" raise FileNotFoundError(f\"Missing CSV: {csv_path}. Run the script first.\")\n",
"\n",
"df = pd.read_csv(csv_path)\n",
"\n",
"event_summary_cols = {\n",
" \"event_name\",\n",
" \"event_key\",\n",
" \"root_tu_count\",\n",
" \"selected_avg_per_root_tu_s\",\n",
" \"selected_total_s\",\n",
"}\n",
"required_cols = [\n",
" \"header_path\",\n",
" \"include_tu_count\",\n",
" \"avg_process_time_s\",\n",
" \"total_process_time_s\",\n",
"]\n",
"\n",
"if event_summary_cols.issubset(df.columns):\n",
" df[\"header_path\"] = df[\"event_key\"]\n",
" df[\"include_tu_count\"] = df[\"root_tu_count\"]\n",
" if (\n",
" \"avg_inclusive_per_root_tu_s\" in df.columns\n",
" and \"total_inclusive_s\" in df.columns\n",
" ):\n",
" df[\"avg_process_time_s\"] = df[\"avg_inclusive_per_root_tu_s\"]\n",
" df[\"total_process_time_s\"] = df[\"total_inclusive_s\"]\n",
" else:\n",
" df[\"avg_process_time_s\"] = df[\"selected_avg_per_root_tu_s\"]\n",
" df[\"total_process_time_s\"] = df[\"selected_total_s\"]\n",
"\n",
"missing = [c for c in required_cols if c not in df.columns]\n",
"if missing:\n",
" raise ValueError(\n",
" f\"CSV is not all-header processing output. Missing columns: {missing}. \"\n",
" \"Run the script cell above to regenerate /tmp/compile_time.csv.\"\n",
" )\n",
"\n",
"for col in [\"include_tu_count\", \"avg_process_time_s\", \"total_process_time_s\"]:\n",
" df[col] = pd.to_numeric(df[col], errors=\"coerce\").fillna(0)\n",
"\n",
"print(f\"Rows: {len(df):,}\")\n",
"print(f\"Columns: {list(df.columns)}\")\n",
"df.head(5)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1a9b36a8",
"metadata": {},
"outputs": [],
"source": [
"# --- Quick top-N views ---\n",
"TOP_N = 5\n",
"\n",
"print(\"Top by avg_process_time_s\")\n",
"display(\n",
" df.nlargest(TOP_N, \"avg_process_time_s\")[\n",
" [\n",
" \"header_path\",\n",
" \"avg_process_time_s\",\n",
" \"include_tu_count\",\n",
" \"total_process_time_s\",\n",
" ]\n",
" ]\n",
")\n",
"\n",
"print(\"\\nTop by include_tu_count\")\n",
"display(\n",
" df.nlargest(TOP_N, \"include_tu_count\")[\n",
" [\n",
" \"header_path\",\n",
" \"include_tu_count\",\n",
" \"avg_process_time_s\",\n",
" \"total_process_time_s\",\n",
" ]\n",
" ]\n",
")\n",
"\n",
"print(\"\\nTop by impact score (include_tu_count * avg_process_time_s)\")\n",
"df_score = df.copy()\n",
"df_score[\"impact_score\"] = df_score[\"include_tu_count\"] * df_score[\"avg_process_time_s\"]\n",
"display(\n",
" df_score.nlargest(TOP_N, \"impact_score\")[\n",
" [\n",
" \"header_path\",\n",
" \"impact_score\",\n",
" \"include_tu_count\",\n",
" \"avg_process_time_s\",\n",
" \"total_process_time_s\",\n",
" ]\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
"id": "174eabd4",
"metadata": {},
"source": [
"### How to read this plot\n",
"\n",
"- Each point is one header seen in all-mode profiling.\n",
"- **X axis (`include_tu_count`)**: how many generated public-header TUs include this header at least once.\n",
"- **Y axis (`avg_process_time_s`)**: average time spent processing that header per including TU.\n",
"- Headers near the **upper-right** are usually the best optimization candidates because they are both widespread and expensive per include."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9ddc0725",
"metadata": {},
"outputs": [],
"source": [
"# --- Interactive scatter (hover shows header name) ---\n",
"\n",
"import plotly.express as px\n",
"\n",
"plot_df = df.copy()\n",
"\n",
"total_headers = len(plot_df)\n",
"total_public_headers = int(plot_df[\"include_tu_count\"].max())\n",
"\n",
"fig = px.scatter(\n",
" plot_df,\n",
" x=\"include_tu_count\",\n",
" y=\"avg_process_time_s\",\n",
" hover_name=\"header_path\",\n",
" hover_data={\n",
" \"include_tu_count\": True,\n",
" \"avg_process_time_s\": \":.6f\",\n",
" \"total_process_time_s\": \":.3f\",\n",
" \"header_path\": False,\n",
" },\n",
" opacity=0.6,\n",
" title=(\n",
" f\"Header include count vs avg processing time ({total_headers:,} total headers; \"\n",
" f\"coverage measured across {total_public_headers} public headers)\"\n",
" ),\n",
")\n",
"\n",
"fig.update_layout(\n",
" xaxis_title=(\n",
" \"TU coverage: number of public headers that include this header \"\n",
" f\"(out of {total_public_headers})\"\n",
" ),\n",
" yaxis_title=\"Average processing time per including TU (seconds)\",\n",
" height=900,\n",
")\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f429b934",
"metadata": {},
"outputs": [],
"source": [
"# --- Optional: run ctadvisor and parse expensive headers ---\n",
"run_ctadvisor = os.environ.get(\"COMPILE_TIME_RUN_CTADVISOR\") == \"1\"\n",
"CTADVISOR_ENTRIES = 10\n",
"CTADVISOR_THREADS = os.cpu_count() or 8\n",
"\n",
"trace_root = (\n",
" REPO_ROOT\n",
" / \"build\"\n",
" / os.environ.get(\"CCCL_BUILD_INFIX\", \"cuda13.1-gcc14\")\n",
" / os.environ.get(\"CCCL_COMPILE_TIME_PRESET\", \"all-dev\")\n",
" / \"compile_time\"\n",
" / \"raw_traces\"\n",
")\n",
"ctadvisor_cmd = [\n",
" \"ctadvisor\",\n",
" \"--trace-file-path\",\n",
" str(trace_root),\n",
" \"--header-advisor-entries\",\n",
" str(CTADVISOR_ENTRIES),\n",
" \"--thread-number\",\n",
" str(CTADVISOR_THREADS),\n",
"]\n",
"\n",
"if run_ctadvisor:\n",
" print(\"Command:\")\n",
" print(\" \" + \" \".join(shlex.quote(x) for x in ctadvisor_cmd))\n",
"\n",
" result = subprocess.run(\n",
" ctadvisor_cmd, cwd=REPO_ROOT, check=True, capture_output=True, text=True\n",
" )\n",
" print(result.stdout)\n",
"else:\n",
" print(\"Skipping ctadvisor. Set COMPILE_TIME_RUN_CTADVISOR=1 to run it.\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "cccl",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,272 @@
#!/usr/bin/env python3
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
try:
import yaml
except ModuleNotFoundError:
yaml = None
YAML_ERROR_TYPES: tuple[type[BaseException], ...] = ()
else:
YAML_ERROR_TYPES = (yaml.YAMLError,)
ID_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]*$")
TIMINGS = {"inclusive", "exclusive"}
SORTS = {"total", "avg", "avg-root-tu", "max"}
def die(message: str) -> None:
print(f"error: {message}", file=sys.stderr)
raise SystemExit(2)
def require_mapping(value: Any, where: str) -> dict[str, Any]:
if not isinstance(value, dict):
die(f"{where} must be a mapping")
return value
def require_field(mapping: dict[str, Any], field: str, where: str) -> Any:
if field not in mapping:
die(f"{where} is missing required field '{field}'")
return mapping[field]
def require_string(value: Any, where: str, *, nonempty: bool = True) -> str:
if not isinstance(value, str):
die(f"{where} must be a string")
if nonempty and not value:
die(f"{where} must be non-empty")
return value
def require_id(value: Any, where: str) -> str:
text = require_string(value, where)
if not ID_RE.fullmatch(text):
die(f"{where} must match {ID_RE.pattern}")
return text
def require_string_list(value: Any, where: str) -> list[str]:
if not isinstance(value, list) or not value:
die(f"{where} must be a non-empty list")
strings: list[str] = []
for index, item in enumerate(value):
strings.append(require_string(item, f"{where}[{index}]"))
return strings
def require_bool(value: Any, where: str) -> bool:
if not isinstance(value, bool):
die(f"{where} must be a boolean")
return value
def require_positive_int(value: Any, where: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
die(f"{where} must be a positive integer")
return value
def validate_slice(
slice_data: Any,
*,
where: str,
seen_ids: set[str],
) -> dict[str, Any]:
data = require_mapping(slice_data, where)
slice_id = require_id(require_field(data, "id", where), f"{where}.id")
if slice_id in seen_ids:
die(f"duplicate slice id '{slice_id}' in {where}")
seen_ids.add(slice_id)
title = require_string(require_field(data, "title", where), f"{where}.title")
filter_name = require_string(
require_field(data, "filter", where), f"{where}.filter"
)
timing = require_string(require_field(data, "timing", where), f"{where}.timing")
if timing not in TIMINGS:
die(f"{where}.timing must be one of {sorted(TIMINGS)}")
sort = require_string(require_field(data, "sort", where), f"{where}.sort")
if sort not in SORTS:
die(f"{where}.sort must be one of {sorted(SORTS)}")
top = require_field(data, "top", where)
if isinstance(top, bool) or not isinstance(top, int) or top <= 0:
die(f"{where}.top must be a positive integer")
threshold = require_field(data, "threshold", where)
if (
isinstance(threshold, bool)
or not isinstance(threshold, (int, float))
or threshold < 0
):
die(f"{where}.threshold must be a non-negative number")
result: dict[str, Any] = {
"id": slice_id,
"title": title,
"filter": filter_name,
"timing": timing,
"sort": sort,
"top": top,
"threshold": threshold,
}
for optional in ("scope_filter", "exclusive_scope"):
if optional in data:
result[optional] = require_string(
data[optional], f"{where}.{optional}", nonempty=False
)
children = data.get("children", [])
if not isinstance(children, list):
die(f"{where}.children must be a list")
if children:
result["children"] = [
validate_slice(
child,
where=f"{where}.children[{index}]",
seen_ids=seen_ids,
)
for index, child in enumerate(children)
]
return result
def validate_config(
config_data: Any, *, where: str, seen_ids: set[str]
) -> dict[str, Any]:
data = require_mapping(config_data, where)
config_id = require_id(require_field(data, "id", where), f"{where}.id")
if config_id in seen_ids:
die(f"duplicate compile_time config id '{config_id}'")
seen_ids.add(config_id)
targets = require_string_list(
require_field(data, "targets", where), f"{where}.targets"
)
slices = require_field(data, "slices", where)
if not isinstance(slices, list) or not slices:
die(f"{where}.slices must be a non-empty list")
slice_ids: set[str] = set()
normalized_slices = [
validate_slice(
slice_data,
where=f"{where}.slices[{index}]",
seen_ids=slice_ids,
)
for index, slice_data in enumerate(slices)
]
return {
"id": config_id,
"name": require_string(require_field(data, "name", where), f"{where}.name"),
"gpu": require_string(require_field(data, "gpu", where), f"{where}.gpu"),
"launch_args": require_string(
require_field(data, "launch_args", where), f"{where}.launch_args"
),
"baseline_ref": require_string(
require_field(data, "baseline_ref", where), f"{where}.baseline_ref"
),
"preset": require_string(
require_field(data, "preset", where), f"{where}.preset"
),
"targets": targets,
"args": require_string(data.get("args", ""), f"{where}.args", nonempty=False),
"comment": require_bool(data.get("comment", True), f"{where}.comment"),
"artifact_retention_days": require_positive_int(
data.get("artifact_retention_days", 14),
f"{where}.artifact_retention_days",
),
"slices": normalized_slices,
}
def matrix_entry(config: dict[str, Any]) -> dict[str, Any]:
config_id = config["id"]
return {
"id": config_id,
"name": config["name"],
"gpu": config["gpu"],
"launch_args": config["launch_args"],
"baseline_ref": config["baseline_ref"],
"preset": config["preset"],
"targets_json": json.dumps(config["targets"], separators=(",", ":")),
"args": config["args"],
"slices_json": json.dumps({"slices": config["slices"]}, separators=(",", ":")),
"comment": str(config["comment"]).lower(),
"artifact_retention_days": config["artifact_retention_days"],
"comment_header": f"compile-time-bench-{config_id}",
}
def parse_matrix(path: Path, workflow: str) -> dict[str, Any]:
try:
if yaml is not None:
with path.open(encoding="utf-8") as f:
matrix = yaml.safe_load(f) or {}
else:
completed = subprocess.run(
["yq", "-o=json", ".", path.as_posix()],
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
matrix = json.loads(completed.stdout or "{}")
except OSError as e:
die(f"failed to read {path}: {e}")
except subprocess.CalledProcessError as e:
die(f"failed to parse {path} with yq: {e.stderr.strip()}")
except json.JSONDecodeError as e:
die(f"failed to decode {path} as JSON: {e}")
except YAML_ERROR_TYPES as e:
die(f"failed to parse {path}: {e}")
compile_time = matrix.get("compile_time")
if compile_time is None:
return {"include": []}
compile_time = require_mapping(compile_time, "compile_time")
configs = compile_time.get(workflow, [])
if configs is None:
configs = []
if not isinstance(configs, list):
die(f"compile_time.{workflow} must be a list")
if not configs:
return {"include": []}
seen_ids: set[str] = set()
return {
"include": [
matrix_entry(
validate_config(
config,
where=f"compile_time.{workflow}[{index}]",
seen_ids=seen_ids,
)
)
for index, config in enumerate(configs)
]
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Parse ci/matrix.yaml compile_time entries for GitHub Actions."
)
parser.add_argument("matrix_yaml", type=Path)
parser.add_argument("--workflow", default="pull_request")
args = parser.parse_args()
json.dump(parse_matrix(args.matrix_yaml, args.workflow), sys.stdout)
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
DETAIL_EVENT_NAMES = {
"Code Generation Function",
"CodeGen Function",
"ExecuteCompiler",
"Frontend",
"Instantiating Template Class",
"Instantiating Template Function",
"InstantiateClass",
"InstantiateFunction",
"OptFunction",
"ParseClass",
"PerformPendingInstantiations",
"Processing Header File",
"RunPass",
"Scanning Function Body",
"Source",
}
DETAIL_PREFIXES_TO_STRIP = (
"libcudacxx/include/",
"cudax/include/",
"c/parallel/include/",
)
DETAIL_PREFIXES_TO_COLLAPSE = (
("cub/cub/", "cub/"),
("thrust/thrust/", "thrust/"),
)
def normalize_detail(detail: str, repo_root: Path) -> str:
detail_path = Path(detail)
if detail_path.is_absolute():
try:
rel = detail_path.resolve(strict=False).relative_to(repo_root)
detail = rel.as_posix()
except ValueError:
pass
for prefix in DETAIL_PREFIXES_TO_STRIP:
if detail.startswith(prefix):
detail = detail[len(prefix) :]
break
for prefix, replacement in DETAIL_PREFIXES_TO_COLLAPSE:
if detail.startswith(prefix):
detail = replacement + detail[len(prefix) :]
break
return detail
def display_detail(detail: str, repo_root: Path, max_detail_len: int | None) -> str:
detail = normalize_detail(detail, repo_root)
if (
max_detail_len is not None
and max_detail_len > 0
and len(detail) > max_detail_len
):
return detail[: max_detail_len - 1] + "..."
return detail
def rewrite_event_name(
event: dict, repo_root: Path, max_detail_len: int | None
) -> bool:
name = event.get("name")
if name not in DETAIL_EVENT_NAMES:
return False
args = event.get("args")
if not isinstance(args, dict):
return False
detail = args.get("detail")
if not detail:
return False
args.setdefault("original_name", name)
event["name"] = f"{name}: {display_detail(str(detail), repo_root, max_detail_len)}"
return True
def prepare_trace(
input_path: Path, output_path: Path, repo_root: Path, max_detail_len: int | None
) -> int:
with input_path.open(encoding="utf-8") as f:
trace = json.load(f)
rewritten = 0
for event in trace.get("traceEvents", []):
if rewrite_event_name(event, repo_root, max_detail_len):
rewritten += 1
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as f:
json.dump(trace, f, separators=(",", ":"))
return rewritten
def iter_input_traces(input_path: Path) -> list[Path]:
if input_path.is_file():
return [input_path]
return sorted(input_path.rglob("*.json"))
def output_path_for(input_trace: Path, input_root: Path, output_path: Path) -> Path:
if input_root.is_file():
if output_path.is_dir() or not output_path.suffix:
return output_path / f"{input_trace.stem}.perfetto.json"
return output_path
rel = input_trace.relative_to(input_root)
return output_path / rel.parent / f"{rel.stem}.perfetto.json"
def main() -> None:
parser = argparse.ArgumentParser(
description="Prepare NVCC device-time-trace JSON files for Perfetto by promoting args.detail into event names."
)
parser.add_argument(
"--input", required=True, type=Path, help="Input trace JSON file or directory"
)
parser.add_argument(
"--output", required=True, type=Path, help="Output trace JSON file or directory"
)
parser.add_argument(
"--repo-root", default=Path(__file__).resolve().parents[2], type=Path
)
parser.add_argument(
"--max-detail-len",
default=0,
type=int,
help="Truncate promoted detail text to this many characters; 0 keeps full details",
)
args = parser.parse_args()
input_path = args.input.resolve(strict=False)
output_path = args.output.resolve(strict=False)
repo_root = args.repo_root.resolve(strict=False)
max_detail_len = args.max_detail_len if args.max_detail_len > 0 else None
traces = iter_input_traces(input_path)
if not traces:
raise SystemExit(f"no JSON traces found under {args.input}")
total_rewritten = 0
for trace_path in traces:
total_rewritten += prepare_trace(
trace_path,
output_path_for(trace_path, input_path, output_path),
repo_root,
max_detail_len,
)
print(f"prepared {len(traces)} trace(s); renamed {total_rewritten} event(s)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,246 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
from typing import Any
def load_json(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as f:
payload = json.load(f)
if not isinstance(payload, dict):
raise SystemExit(f"{path} must contain a JSON object")
return payload
def md_escape(value: object) -> str:
text = str(value)
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("|", "\\|")
.replace("\n", " ")
)
def md_code_span(value: object) -> str:
text = str(value).replace("\n", " ")
max_backtick_run = 0
current_backtick_run = 0
for char in text:
if char == "`":
current_backtick_run += 1
max_backtick_run = max(max_backtick_run, current_backtick_run)
else:
current_backtick_run = 0
delimiter = "`" * (max_backtick_run + 1)
if text.startswith("`") or text.endswith("`"):
text = f" {text} "
return f"{delimiter}{text}{delimiter}"
def render_event_name(row: dict[str, Any]) -> str:
event_name = row.get("event_name", "")
event_key = row.get("event_key", "")
if event_key:
return f"{md_escape(event_name)}: {md_code_span(event_key)}"
return md_escape(event_name)
def render_rows(rows: list[dict[str, Any]], *, direction: str) -> str:
delta_heading = (
"Regression impact" if direction == "worse" else "Improvement impact"
)
lines = [
f"| Rank | {delta_heading} | Selected Δ | Baseline | Current | Event | Matched traces |",
"| ---: | ---: | ---: | ---: | ---: | --- | ---: |",
]
for row in rows:
lines.append(
"| {rank} | `{impact}` | `{selected_delta}` | `{baseline}` | `{current}` | {event} | {traces} |".format(
rank=md_escape(row.get("rank", "")),
impact=md_escape(row.get("impact_magnitude_s", "")),
selected_delta=md_escape(row.get("selected_delta_s", "")),
baseline=md_escape(row.get("baseline_selected_s", "")),
current=md_escape(row.get("current_selected_s", "")),
event=render_event_name(row),
traces=md_escape(row.get("matched_trace_count", "")),
)
)
return "\n".join(lines)
def render_direction_details(
slice_title: str,
direction: str,
rows: list[dict[str, Any]],
) -> str:
if not rows:
return ""
label = "Regressions" if direction == "worse" else "Improvements"
icon = "🔴" if direction == "worse" else "🟢"
return "\n".join(
[
"<details>",
f"<summary><strong>{icon} {md_escape(slice_title)}{label}</strong></summary>",
"",
render_rows(rows, direction=direction),
"",
"</details>",
]
)
def render_warning_details(slice_title: str, warnings: list[Any]) -> str:
if not warnings:
return ""
lines = [
"<details open>",
f"<summary><strong>⚠️ {md_escape(slice_title)} — Warnings</strong></summary>",
"",
]
lines.extend(f"- {md_escape(warning)}" for warning in warnings)
lines.extend(["", "</details>"])
return "\n".join(lines)
def render_slice(slice_data: dict[str, Any], *, level: int = 3) -> str:
comparison = slice_data.get("comparison", {})
worse_rows = comparison.get("worse", {}).get("rows", [])
better_rows = comparison.get("better", {}).get("rows", [])
warnings = slice_data.get("warnings", [])
child_sections = [
rendered
for child in slice_data.get("children", [])
if (rendered := render_slice(child, level=level + 1))
]
direct_sections = [
section
for section in (
render_warning_details(slice_data.get("title", "Slice"), warnings),
render_direction_details(
slice_data.get("title", "Slice"), "worse", worse_rows
),
render_direction_details(
slice_data.get("title", "Slice"), "better", better_rows
),
)
if section
]
if not direct_sections and not child_sections:
return ""
heading_prefix = "#" * min(level, 6)
subtitle = (
f"`-f {slice_data.get('filter', '')}` "
f"`{slice_data.get('timing', '')}` "
f"`--sort {slice_data.get('sort', '')}`"
)
lines = [
f"{heading_prefix} {md_escape(slice_data.get('title', 'Slice'))}",
"",
subtitle,
"",
]
lines.extend(join_sections(direct_sections))
if child_sections:
lines.extend(["", *join_sections(child_sections)])
return "\n".join(lines).strip()
def join_sections(sections: list[str]) -> list[str]:
lines: list[str] = []
for section in sections:
if lines:
lines.append("")
lines.append(section)
return lines
def count_rows(slice_data: dict[str, Any], direction: str) -> int:
comparison = slice_data.get("comparison", {})
total = len(comparison.get(direction, {}).get("rows", []))
return total + sum(
count_rows(child, direction) for child in slice_data.get("children", [])
)
def count_warnings(slice_data: dict[str, Any]) -> int:
return len(slice_data.get("warnings", [])) + sum(
count_warnings(child) for child in slice_data.get("children", [])
)
def render_comment(
summary: dict[str, Any],
config: dict[str, Any],
*,
artifacts_url: str,
) -> str:
config_id = str(config["id"])
slices = summary.get("slices", [])
sections = [
section for slice_data in slices if (section := render_slice(slice_data))
]
worse_count = sum(count_rows(slice_data, "worse") for slice_data in slices)
better_count = sum(count_rows(slice_data, "better") for slice_data in slices)
warning_count = sum(count_warnings(slice_data) for slice_data in slices)
result = (
f"**Result:** {worse_count} regression row(s), "
f"{better_count} improvement row(s) above threshold."
)
if warning_count:
result += f" {warning_count} warning(s)."
lines = [
f"<!-- cccl-compile-time-bench: {md_escape(config_id)} -->",
f"## ⏱️ CCCL compile-time benchmark comparison: {md_escape(config.get('name', config_id))}",
"",
result,
"",
"| Run | Value |",
"| --- | --- |",
f"| Config | {md_code_span(config_id)} |",
f"| Baseline | {md_code_span(config.get('baseline_ref', ''))} |",
f"| Preset | {md_code_span(config.get('preset', ''))} |",
f"| Targets | {md_code_span(', '.join(config.get('targets', [])))} |",
f"| GPU / launch args | {md_code_span(config.get('gpu', ''))} / {md_code_span(config.get('launch_args', ''))} |",
"",
f"**Artifacts:** [reports and traces]({artifacts_url})",
"",
]
if sections:
lines.extend(join_sections(sections))
else:
lines.append(
"No compile-time benchmark changes exceeded the configured thresholds."
)
return "\n".join(lines).rstrip() + "\n"
def main() -> None:
parser = argparse.ArgumentParser(
description="Render a GitHub PR comment from compile-time report JSON."
)
parser.add_argument("--summary", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--artifacts-url", required=True)
parser.add_argument("-o", "--output", type=Path)
args = parser.parse_args()
comment = render_comment(
load_json(args.summary),
load_json(args.config),
artifacts_url=args.artifacts_url,
)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(comment, encoding="utf-8")
else:
print(comment, end="")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
import argparse
import csv
import subprocess
from pathlib import Path
GENERATED_TU_MARKER = "/headers/"
GENERATED_TU_SOURCE_SUFFIXES = (".cu", ".cpp", ".cxx", ".cc", ".c")
PREPROCESSED_TU_SUFFIX = ".cpp4.ii"
PREPROCESSED_TU_SUFFIXES = (".cpp4.ii", ".ii")
def strip_generated_tu_suffix(path_text: str) -> str:
for suffix in GENERATED_TU_SOURCE_SUFFIXES:
if path_text.endswith(suffix):
return path_text[: -len(suffix)]
return path_text
def generated_tu_input(tu: Path) -> str:
parts = tu.as_posix().split(GENERATED_TU_MARKER, 1)
if len(parts) != 2:
return tu.as_posix()
rel = parts[1].split("/", 1)
if len(rel) != 2:
return tu.as_posix()
return strip_generated_tu_suffix(rel[1])
def find_preprocessed_tus(build_dir: Path) -> list[Path]:
return sorted(
{
path
for suffix in PREPROCESSED_TU_SUFFIXES
for path in build_dir.glob(f"**/headers/**/*{suffix}")
}
)
def tu_source_for_preprocessed_tu(pp_path: Path) -> Path:
pp_text = pp_path.as_posix()
for suffix in PREPROCESSED_TU_SUFFIXES:
if pp_text.endswith(suffix):
return Path(pp_text[: -len(suffix)])
return pp_path.with_suffix("")
def run_cloc(preprocessed_tus: list[Path], processes: int) -> dict[str, int]:
if not preprocessed_tus:
return {}
command = [
"cloc",
"--csv",
"--by-file",
"--skip-uniqueness",
"--processes",
str(processes),
"--force-lang=C++,ii",
*[path.as_posix() for path in preprocessed_tus],
]
result = subprocess.run(command, check=True, capture_output=True, text=True)
loc_by_file: dict[str, int] = {}
reader = csv.reader(result.stdout.splitlines())
for row in reader:
if len(row) < 5 or row[1] == "filename":
continue
try:
loc_by_file[row[1]] = int(row[4])
except ValueError:
continue
return loc_by_file
def write_summary(
output_csv: Path,
preprocessed_tus: list[Path],
loc_by_file: dict[str, int],
) -> None:
output_csv.parent.mkdir(parents=True, exist_ok=True)
with output_csv.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(
f,
fieldnames=[
"tu_input",
"transitive_loc",
"tu_source",
"preprocessed_tu",
],
)
writer.writeheader()
for pp_path in preprocessed_tus:
tu_path = tu_source_for_preprocessed_tu(pp_path)
writer.writerow(
{
"tu_input": generated_tu_input(tu_path),
"transitive_loc": loc_by_file.get(pp_path.as_posix(), 0),
"tu_source": tu_path.as_posix(),
"preprocessed_tu": pp_path.as_posix(),
}
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Summarize generated TU inputs and preprocessed LOC."
)
parser.add_argument("--build-dir", required=True, type=Path)
parser.add_argument("--output-csv", required=True, type=Path)
parser.add_argument(
"--cloc-processes",
type=int,
default=0,
help="cloc process count; 0 uses nproc --all --ignore=2 when available",
)
args = parser.parse_args()
build_dir = args.build_dir.resolve(strict=False)
preprocessed_tus = find_preprocessed_tus(build_dir)
if not preprocessed_tus:
raise SystemExit(f"no preprocessed generated TUs found under {build_dir}")
processes = args.cloc_processes
if processes <= 0:
try:
processes = int(
subprocess.check_output(
["nproc", "--all", "--ignore=2"], text=True
).strip()
)
except (subprocess.SubprocessError, ValueError):
processes = 1
write_summary(
output_csv=args.output_csv,
preprocessed_tus=preprocessed_tus,
loc_by_file=run_cloc(preprocessed_tus, processes),
)
print(f"wrote {len(preprocessed_tus)} generated TU row(s) to {args.output_csv}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff