diff --git a/.gitignore b/.gitignore index 0c2114c..77f8421 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ src/TA_main2main_workflow/workspace/ src/TA_main2main_workflow/output/ src/TA_main2main_workflow/__pycache__/ *.pyc +.vscode/ diff --git a/README.md b/README.md index ce162b9..cbe6d13 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,40 @@ -# TA Main2Main Upgrade Flow +# TA Main2Main Workflow -Automate triton-ascend's main2main upgrade against upstream Triton. +Automate triton-ascend's upstream sync against Triton main branch. Each time Triton's `main` advances, triton-ascend must catch up: merge -upstream changes, resolve conflicts, fix broken interfaces, build, and run -e2e tests. This project drives that whole loop: +upstream changes, resolve conflicts, fix broken interfaces, rebuild LLVM (if +needed), build triton-ascend, and run tests. This project drives that whole +loop with AI-assisted fix/retry. -- detect the commit gap between triton-ascend and upstream Triton -- create a work branch from the latest triton-ascend main -- merge the target upstream commit into the work branch -- run AI (opencode or claude) to resolve conflicts and fix build/test failures -- run pytest on Ascend NPU, retry on failure (up to 3×) -- when everything passes, optionally push a branch and open a PR +## Pipeline -Full walkthrough lives in [`docs/guide.md`](docs/guide.md); this README only -covers how to install and run. +``` +prepare → detect → plan → per-step loop: + merge → [resolve] → build ⇄ fix → test ⇄ fix → commit + → finalize → [push PR] +``` + +| Phase | Description | +|-------|-------------| +| prepare | Clone triton-ascend, configure `origin` / `triton-upstream` remotes, fetch, checkout base branch | +| detect | Find merge-base between ascend HEAD and upstream target, list commits to merge | +| plan | Group commits into steps by line budget, handle LLVM-hash changes as solo steps | +| merge | `git merge --no-ff` the step's end commit into the current branch | +| resolve | AI resolves merge conflicts (skipped if no conflicts) | +| build | Rebuild LLVM (with AI fix if needed), then build triton-ascend. Retries on failure | +| test | Run pytest on Ascend NPU. Retries with AI fix on failure | +| commit | `git add -A && git commit -s` with structured message | +| finalize | Generate cumulative patch and summary report | +| push PR | Push branch and create GitHub PR (opt-in) | ## Requirements -- Python 3.10–3.13 -- [`opencode`](https://opencode.ai) or `claude` CLI on `$PATH` (used as the AI adapter) -- `git`, plus local checkouts of `triton` and `triton-ascend` -- For real e2e tests: a host with Ascend NPUs -- For automated PRs: [`gh`](https://cli.github.com/) logged in -- LLVM toolchain (for building triton-ascend C++ extensions) +- Python 3.10+ +- `opencode` or `claude` CLI on `$PATH` (AI adapter) +- `git`, `cmake`, `ninja`, `clang`/`clang++` (for LLVM build) +- For tests: a host with Ascend NPUs +- For auto PR: `gh` CLI logged in ## Install @@ -31,153 +42,146 @@ covers how to install and run. pip install -e . ``` -This registers the `ta-kickoff` and `ta-plot` console scripts. - -## Run - -```bash -ta-kickoff \ - --triton-ascend-path /path/to/triton-ascend \ - --triton-path /path/to/triton \ - [--target-commit <40-char SHA>] -``` - -- Both paths must be local git checkouts. -- `--target-commit` is optional; defaults to upstream triton `HEAD`. -- Each run wipes and recreates `workspace/` inside the installed package directory - (under `src/TA_main2main_workflow/` in editable installs, or - `/TA_main2main_workflow/` with `pip install`). - Use `TA_MAIN2MAIN_WORKSPACE` env var to override the location. - -CLI flags can also be supplied via env vars: `TRITON_ASCEND_PATH`, `TRITON_PATH`, -`TRITON_TARGET_COMMIT` (CLI wins). +Registers the `ta-kickoff` console script. -### Common variations +## Quick Start ```bash -# Dry-run plumbing: skip both AI and NPU tests -SKIP_AI_ANALYSIS=true SKIP_E2E_TEST=true ta-kickoff \ - --triton-ascend-path /path/to/triton-ascend --triton-path /path/to/triton - -# Target a specific upstream commit -ta-kickoff \ - --triton-ascend-path /path/to/triton-ascend \ - --triton-path /path/to/triton \ - --target-commit abc123def456 - -# Auto-push a branch and open a PR after a successful run -PUSH_TO_GITHUB=true GITHUB_REPO=triton-lang/triton-ascend \ -ta-kickoff --triton-ascend-path ... --triton-path ... - -# Custom PR title: [jeshd](sync) merge upstream triton commits -PR_AUTHOR=jeshd PR_TYPE=sync PUSH_TO_GITHUB=true \ -ta-kickoff --triton-ascend-path ... --triton-path ... -``` - -### PR title format +# Auto-clone triton-ascend to workspace/, sync to a specific upstream commit +ta-kickoff --target-commit 99f44dd5a90c9ae30daa974704fcea0bcc4f5ba1 -PR titles follow the pattern `[user](type) description`: +# Use existing local repo +ta-kickoff --triton-ascend-path /path/to/triton-ascend --target-commit abc123 -``` -[TA](sync) merge upstream triton commits (20240612-120000) +# Dry-run: skip AI and tests (verify merge + build only) +SKIP_AI_ANALYSIS=true SKIP_BUILD=false SKIP_E2E_TEST=true ta-kickoff --target-commit abc123 ``` -- `user`: from `PR_AUTHOR` env var, falls back to `git config user.name`, then `TA` -- `type`: from `PR_TYPE` env var, defaults to `sync` - -### Pre-commit before PR - -Before pushing and creating a PR, the flow runs: -```bash -pre-commit run --from-ref origin/main --to-ref HEAD -``` - -If pre-commit auto-fixes files (e.g., formatting), those changes are -automatically amended into the latest commit with `git commit --amend --no-edit`. -Temp files (`result_profiling/`, `__pycache__/`, `*.lock`, `*.pyc`) are -cleaned both before and after pre-commit to avoid accidentally committing them. - -### Environment variables - -| Variable | Purpose | Default | -|---|---|---| -| `TRITON_ASCEND_PATH` | triton-ascend repo path | cwd | -| `TRITON_PATH` | upstream triton repo path | cwd | -| `TRITON_TARGET_COMMIT` | target triton commit SHA | triton `HEAD` | -| `AI_BACKEND` | AI adapter: `opencode` or `claude` | auto-detect | -| `SKIP_AI_ANALYSIS` | skip AI, only run deterministic steps | `false` | -| `SKIP_BUILD` | skip the build step | `false` | -| `SKIP_E2E_TEST` | skip pytest, treat as passed | `false` | -| `PUSH_TO_GITHUB` | push & create PR after all steps pass | `false` | -| `GITHUB_REPO` | PR target, `owner/name` | `TecJesh/triton-ascend` | -| `PR_AUTHOR` | user tag in PR title, e.g. `[TA](sync) ...` | git `user.name` or `TA` | -| `PR_TYPE` | conventional commit type in PR title | `sync` | -| `LLVM_INSTALL_PREFIX` | LLVM install path for building | — | -| `CONDA_ENV` | Conda environment name | `ta-upgrade` | -| `NUM_PROCS` | parallel pytest workers | `16` | -| `AUTO_STASH` | auto-stash before sync | `false` | -| `TA_PROGRESSIVE_MERGE` | enable progressive step merge | `true` | -| `TA_LINE_BUDGET` | max source lines per step | `1000` | -| `TA_COMMIT_BUDGET` | base commit-count budget per step | `5` | -| `TA_MAIN2MAIN_WORKSPACE` | override workspace directory path | package dir | - -## Outputs - -Everything lands under `workspace/` inside the installed package directory. -Override with `TA_MAIN2MAIN_WORKSPACE` env var. Default location: - -- **Editable install** (`pip install -e .`): `src/TA_main2main_workflow/workspace/` -- **Regular install** (`pip install .`): `/lib/.../site-packages/TA_main2main_workflow/workspace/` +## CLI Arguments + +| Argument | Env Variable | Default | Description | +|----------|-------------|---------|-------------| +| `--triton-ascend-path` | `TRITON_ASCEND_PATH` | — | Local path to triton-ascend repo (auto-clone if not set) | +| `--triton-path` | `TRITON_PATH` | — | Local triton repo (offline mode, not used in remote mode) | +| `--target-commit` | `TRITON_TARGET_COMMIT` | triton-upstream/main HEAD | Upstream commit SHA to sync to | +| `--llvm-prefix` | `LLVM_INSTALL_PREFIX` | `workspace/llvm-install` | LLVM install prefix path | +| `--build-procs` | `BUILD_PROCS` | 32 | Parallel workers for ninja / cmake build | +| `--test-procs` | `TEST_PROCS` | 8 | Parallel pytest workers (`-n`) | + +## Environment Variables + +### Repository + +| Variable | Default | Description | +|----------|---------|-------------| +| `TRITON_ASCEND_PATH` | — | Local triton-ascend path; if empty, auto-clone from URL | +| `TRITON_ASCEND_URL` | `https://github.com/triton-lang/triton-ascend.git` | Clone URL for triton-ascend | +| `TRITON_PATH` | — | Local triton repo (offline mode) | +| `TRITON_UPSTREAM_URL` | `https://github.com/triton-lang/triton.git` | Upstream Triton remote URL | +| `TRITON_TARGET_COMMIT` | — | Target upstream commit SHA | +| `TA_BASE_BRANCH` | `upstream_sync` | Base branch in triton-ascend to sync from | +| `LLVM_REPO_URL` | `https://github.com/llvm/llvm-project.git` | LLVM clone URL | +| `LLVM_INSTALL_PREFIX` | `workspace/llvm-install` | LLVM install prefix | + +### AI Backend + +| Variable | Default | Description | +|----------|---------|-------------| +| `AI_BACKEND` | `auto` | AI adapter: `opencode`, `claude`, or `auto` (detect) | +| `TA_AI_TIMEOUT_MINUTES` | 30 | AI call timeout in minutes | +| `TA_AI_STALE_SECONDS` | 1200 | AI stale timeout (seconds) | +| `TA_AI_MAX_STALE_RETRIES` | 3 | Max AI stale retries | + +### Build / Test + +| Variable | Default | Description | +|----------|---------|-------------| +| `BUILD_PROCS` | 32 | Parallel build workers (ninja `-j`, cmake, setup.py) | +| `TEST_PROCS` | 8 | Parallel pytest workers (`-n`) | + +### Retry / Budget + +| Variable | Default | Description | +|----------|---------|-------------| +| `TA_MAX_RETRIES` | 10 | Max retry attempts per step (build + test) | +| `TA_LINE_BUDGET` | 1000 | Max source lines per merge step | +| `TA_PROGRESSIVE_MERGE` | `true` | Enable progressive step merge | +| `TA_IR_MAX_ITERATIONS` | 3 | Max IR patch iterations | + +### Skip Flags + +| Variable | Default | Description | +|----------|---------|-------------| +| `TA_RESUME` | `false` | Skip steps whose output files already exist | +| `SKIP_AI_ANALYSIS` | `false` | Skip all AI calls (conflict resolution, fix) | +| `SKIP_BUILD` | `false` | Skip triton-ascend build | +| `SKIP_E2E_TEST` | `false` | Skip pytest, treat as passed | +| `SKIP_LLVM_REBUILD` | `false` | Skip LLVM rebuild | +| `SKIP_IR_PATCH` | `false` | Skip IR patch generation | +| `SKIP_BASELINE_LLVM` | `false` | Skip baseline LLVM build | + +### Git / PR + +| Variable | Default | Description | +|----------|---------|-------------| +| `PUSH_TO_GITHUB` | `false` | Push branch and create PR after success | +| `GITHUB_REPO` | `triton-lang/triton-ascend` | PR target `owner/name` | +| `TA_MAIN2MAIN_WORKSPACE` | `./workspace` | Override workspace directory | + +## Workspace Layout ``` workspace/ -├── detect.json # merge-base, target commit, changed files -├── merge_result.json # merge status, conflict info -├── merge.log # raw git merge output -├── build_result.json # build step results -├── build.log # raw build output -├── test_result.json # pytest summary +├── triton-ascend/ # auto-cloned (if no local path given) +├── llvm-project/ # auto-cloned LLVM +├── llvm-build/ # LLVM build directory (outside llvm-project) +├── llvm-install/ # LLVM install prefix +├── detect.json # merge-base, target commit, changed files +├── steps.json # step plan +├── steps/ +│ └── step-N/ +│ ├── merge_result.json +│ ├── build_result.json +│ ├── build.log / build.err +│ ├── llvm-cmake.log / llvm-cmake.err +│ ├── llvm-ninja.log / llvm-ninja.err +│ ├── upstream.patch +│ ├── changed_files.txt +│ └── commits.txt ├── test-logs/ -│ ├── pytest.log -│ └── precommit.log -├── conflicts/ # conflict snapshots (if any) -├── fixes/ # per-fix-attempt logs -│ └── fix-/ -├── step-0/ -│ ├── step_summary.md # AI-written summary -│ ├── step_target.patch # cumulative diff -│ └── analysis.md # fix diagnosis -├── final_summary.md # final sync summary -├── final_target.patch # cumulative patch -└── FAILURE.md # failure report (if failed) +│ └── pytest-junit.xml +├── final_summary.md +└── final_target.patch ``` -## Project layout +## Project Layout ``` src/TA_main2main_workflow/ -├── flow.py # CrewAI Flow: nodes, routing, retry loop -├── main.py # `ta-kickoff` / `ta-plot` CLI entrypoints -├── utils.py # filename constants + git helpers + console output +├── flow.py # Pipeline orchestrator +├── main.py # `ta-kickoff` CLI entrypoint +├── pipeline/ +│ ├── prepare.py # Phase 0: workspace setup +│ ├── detect.py # Phase 1: detect upstream commits +│ ├── plan.py # Phase 2: plan merge steps +│ ├── merge.py # Phase 3: git merge +│ ├── resolve.py # Phase 3: AI conflict resolution +│ ├── build.py # Phase 3: LLVM + triton-ascend build +│ ├── test.py # Phase 3: pytest +│ ├── fix.py # Phase 3: AI fix +│ ├── commit.py # Phase 3: commit progress +│ ├── finalize.py # Phase 4: summary + patch +│ ├── pre_ci.py # pre-commit checks +│ └── push_pr.py # push + create PR +├── utils/ +│ ├── config.py # TAConfig dataclass +│ ├── context.py # WorkflowContext dataclass +│ ├── git.py # run_git with built-in retry +│ ├── logging.py # TALogger +│ ├── tracker.py # timed() context manager +│ ├── errors.py # Exception types +│ └── submodule.py # AscendNPU-IR submodule helpers ├── agent/ -│ ├── opencode_adapter.py # spawns `opencode run`, parses JSONL events -│ └── prompt.md # single-agent task prompt -├── reference/ # knowledge base the agent reads at runtime -│ ├── adapt-guide.md -│ ├── code-structure-guide.md -│ ├── diagnosis-guide.md -│ ├── error-pattern-examples.md -│ └── npu-oom-handling.md -└── scripts/ # deterministic helpers (no AI) - ├── build_test.py - ├── detect_commits.py - ├── merge_upstream.py - ├── plan_steps.py # step planner: splits commits by line budget - ├── pre_ci_check.py - ├── push_to_github.py - └── update_commit_reference.py +│ └── opencode_adapter.py # AI adapter (opencode / claude) +└── reference/ # AI knowledge base ``` - -For a step-by-step explanation of every node and the per-step artifacts, see -[`docs/guide.md`](docs/guide.md). For conventions and gotchas that affect code -changes to this repo itself, see [`AGENTS.md`](AGENTS.md). diff --git a/pyproject.toml b/pyproject.toml index 302cb82..42aec77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,16 @@ [project] name = "TA_main2main_workflow" version = "0.1.0" -description = "TA_main2main_workflow — Triton-Ascend upstream sync using CrewAI" +description = "TA_main2main_workflow — Triton-Ascend upstream sync automation" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.14.5" + "pydantic>=2" ] [project.scripts] ta-kickoff = "TA_main2main_workflow.main:kickoff" -ta-plot = "TA_main2main_workflow.main:plot" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" - -[tool.crewai] -type = "flow" diff --git a/src/TA_main2main_workflow/agent/opencode_adapter.py b/src/TA_main2main_workflow/agent/opencode_adapter.py deleted file mode 100644 index c1d9689..0000000 --- a/src/TA_main2main_workflow/agent/opencode_adapter.py +++ /dev/null @@ -1,591 +0,0 @@ -"""AI adapter — spawns opencode or claude subprocess for AI-driven tasks. - -Supports two backends (auto-detected or set via AI_BACKEND env var): - - opencode: `opencode run --format json --dangerously-skip-permissions ` - - claude: `claude -p --dangerously-skip-permissions ` - -Used for both merge conflict resolution and test failure fixing. -All progress is printed to the local console — no CrewAI web UI needed. - -Key design for claude backend: - - Do NOT use proc.communicate() — it blocks until process exit with zero output. - - Instead, write prompt to stdin in a background thread, read stdout line-by-line - in real time, just like the opencode backend. - - Print a heartbeat "." every 15 seconds of silence so the user knows it's alive. - - Same stale-timeout / total-timeout logic as opencode. -""" - -from __future__ import annotations - -import json -import os -import queue -import select -import shutil -import subprocess -import threading -import time -from pathlib import Path -from typing import Any, Literal - -from pydantic import BaseModel, Field - -_PROMPT_PATH = Path(__file__).parent / "prompt.md" - -_TIMEOUT_MINUTES = 30 -_STALE_SECONDS = 1200 -_MAX_STALE_RETRIES = 3 -_HEARTBEAT_INTERVAL = 15 # print "." every 15s of silence (claude only) - - -# ── backend detection ──────────────────────────────────────────────────────── - -def _detect_backend() -> str: - """Detect which AI backend to use. Checks AI_BACKEND env var first, - then falls back to whatever is available on PATH.""" - explicit = os.getenv("AI_BACKEND", "").lower() - if explicit in ("claude", "opencode"): - return explicit - if shutil.which("opencode"): - return "opencode" - if shutil.which("claude"): - return "claude" - raise RuntimeError( - "No AI backend found. Install 'opencode' or 'claude' CLI, " - "or set AI_BACKEND env var." - ) - - -# ── mode labels for display ────────────────────────────────────────────────── - -_MODE_LABELS: dict[str, str] = { - "conflict": "CONFLICT RESOLUTION", - "fix": "TEST/BUILD FAILURE FIX", - "adapt": "CODE ADAPTATION", - "report": "SYNC REPORT", - "ir_analyze_ops": "IR OP ANALYSIS", - "ir_analyze_changes": "IR OP CHANGE ANALYSIS", - "ir_generate_patch": "IR PATCH GENERATION", - "ir_diagnose": "IR FAILURE DIAGNOSIS", -} - - -# ── prompt loader ──────────────────────────────────────────────────────────── - -def _build_prompt(inputs: dict[str, Any]) -> str: - from collections import defaultdict - template = _PROMPT_PATH.read_text(encoding="utf-8") - ctx = defaultdict(str, {k: str(v) for k, v in inputs.items()}) - return template.format_map(ctx) - - -# ── result model ───────────────────────────────────────────────────────────── - -class AIResult(BaseModel): - modified_files: list[str] = Field(default_factory=list) - is_noop: bool = Field(default=False) - step_summary: str = Field(default="") - resolved_conflicts: list[str] = Field(default_factory=list) - fixed_tests: list[str] = Field(default_factory=list) - elapsed_seconds: float = Field(default=0.0) - - -# ── main entry point ───────────────────────────────────────────────────────── - -def run_opencode_adapter(inputs: dict[str, Any]) -> AIResult: - """Run the AI adapter for conflict resolution or test fixing. - - Auto-detects available backend (opencode or claude) and streams - all output to the local console. - """ - backend = _detect_backend() - mode = inputs.get("mode", "unknown") - step_id = inputs.get("step_id", "?") - mode_label = _MODE_LABELS.get(mode, f"AI TASK: {mode}") - - print(f"\n{'═' * 60}", flush=True) - print(f" {mode_label}", flush=True) - print(f" Backend: {backend} | Step: {step_id}", flush=True) - print(f" Time: {time.strftime('%H:%M:%S')}", flush=True) - print(f"{'═' * 60}", flush=True) - - t0 = time.monotonic() - - # AI call: dispatch to claude or opencode backend - if backend == "claude": - result = _run_claude(inputs) - else: - result = _run_opencode(inputs) - - result.elapsed_seconds = time.monotonic() - t0 - - # AI call: print completion summary - icon = "✔" if result.modified_files or result.resolved_conflicts else "○" - print(f"\n {icon} AI task completed in {result.elapsed_seconds:.1f}s", flush=True) - if result.modified_files: - print(f" Modified: {', '.join(result.modified_files)}", flush=True) - if result.resolved_conflicts: - print(f" Resolved conflicts: {', '.join(result.resolved_conflicts)}", flush=True) - if result.is_noop: - print(f" (no changes needed)", flush=True) - - return result - - -# ═══════════════════════════════════════════════════════════════════════════════ -# opencode backend (JSONL streaming) -# ═══════════════════════════════════════════════════════════════════════════════ - -def _run_opencode(inputs: dict[str, Any]) -> AIResult: - """opencode backend: JSONL streaming with stale-timeout retry.""" - base_prompt = _build_prompt(inputs) - prompt = base_prompt - step_dir = inputs.get("step_dir", "") - step_path = Path(step_dir) if step_dir else None - log_path = step_path / "opencode.log" if step_path else None - raw_path = step_path / "opencode_raw.jsonl" if step_path else None - stderr_path = step_path / "opencode_stderr.log" if step_path else None - - if log_path: - log_path.write_text("") - if raw_path: - raw_path.write_text("") - if stderr_path: - stderr_path.write_text("") - - all_lines: list[str] = [] - last_reason: _StopReason | None = None - - for attempt in range(_MAX_STALE_RETRIES + 1): - _print_prompt(prompt, attempt) - if log_path: - _log_prompt(prompt, attempt, log_path) - - lines, reason = _run_opencode_once(prompt, log_path, raw_path, stderr_path) - all_lines.extend(lines) - last_reason = reason - - if reason is None: - break - - if reason == "stale_timeout" and attempt < _MAX_STALE_RETRIES: - retry = attempt + 1 - print(f"\n[opencode] retrying after stale timeout ({retry}/{_MAX_STALE_RETRIES})", flush=True) - prompt = _build_opencode_continue(base_prompt, inputs, retry) - continue - - if stderr_path and stderr_path.exists(): - stderr_content = stderr_path.read_text(encoding="utf-8", errors="replace")[-2000:] - if stderr_content: - print(f"\n[opencode] stderr tail:\n{stderr_content}", flush=True) - break - - result = _build_result(step_path, inputs.get("ascend_path", ""), "".join(all_lines)) - if last_reason and not result.step_summary: - result.step_summary = f"opencode process stopped due to {last_reason}" - return result - - -def _build_opencode_continue(base_prompt: str, inputs: dict[str, Any], retry: int) -> str: - return f"""Continue the task for step {inputs.get('step_id', '')}. - -The previous opencode run produced no output for {_STALE_SECONDS} seconds and -was terminated. This is continuation retry {retry}/{_MAX_STALE_RETRIES}. - -Do not start from scratch. The triton-ascend working tree may already contain -partial changes from the previous attempt. Inspect existing changes, reuse prior -work, and continue from where you left off. - -━━━ ORIGINAL TASK ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -{base_prompt} -""" - - -_StopReason = Literal["stale_timeout", "total_timeout"] - - -def _print_prompt(prompt: str, attempt: int) -> None: - title = "AI TASK PROMPT" if attempt == 0 else f"AI CONTINUE PROMPT #{attempt}" - print(f"\n{'━' * 60}", flush=True) - print(f" {title}", flush=True) - print(f"{'━' * 60}", flush=True) - if len(prompt) > 8000: - print(prompt[:4000]) - print(f"\n... [{len(prompt) - 8000} chars truncated, see log for full prompt] ...\n") - print(prompt[-4000:]) - else: - print(prompt) - print(f"{'━' * 60}\n", flush=True) - - -def _log_prompt(prompt: str, attempt: int, log_path: Path) -> None: - title = "AI TASK PROMPT" if attempt == 0 else f"AI CONTINUE PROMPT #{attempt}" - with log_path.open("a", encoding="utf-8") as fh: - fh.write(f"{'═' * 60}\n{title}:\n{'═' * 60}\n{prompt}\n{'═' * 60}\n\n") - - -def _subprocess_env() -> dict: - """Environment for launching the AI CLI. - - Claude Code refuses `--dangerously-skip-permissions` when running as root - ("cannot be used with root/sudo privileges for security reasons"). In CI - the job runs as root inside a container — already an isolated sandbox — so - we opt in via IS_SANDBOX=1 to allow the flag. Only set it under root, so - local non-root runs are unaffected. - """ - env = os.environ.copy() - if hasattr(os, "geteuid") and os.geteuid() == 0: - env.setdefault("IS_SANDBOX", "1") - return env - - -def _run_opencode_once( - prompt: str, - log_path: Path | None, - raw_path: Path | None, - stderr_path: Path | None, -) -> tuple[list[str], _StopReason | None]: - stderr_fh = stderr_path.open("a", encoding="utf-8") if stderr_path else None - proc = subprocess.Popen( - [ - "opencode", "run", - "--format", "json", - "--dangerously-skip-permissions", - prompt, - ], - stdout=subprocess.PIPE, - stderr=stderr_fh or subprocess.DEVNULL, - text=True, - bufsize=1, - env=_subprocess_env(), - ) - - lines_queue: queue.Queue[str | None] = queue.Queue() - - def _stdout_reader(): - assert proc.stdout is not None - for line in proc.stdout: - lines_queue.put(line) - lines_queue.put(None) - - reader_thread = threading.Thread(target=_stdout_reader, daemon=True) - reader_thread.start() - - state = _EventState() - log_fh = log_path.open("a", encoding="utf-8") if log_path else None - raw_fh = raw_path.open("a", encoding="utf-8") if raw_path else None - - deadline = time.monotonic() + _TIMEOUT_MINUTES * 60 - last_output_time = time.monotonic() - stop_reason: _StopReason | None = None - - try: - while True: - try: - line = lines_queue.get(timeout=1.0) - except queue.Empty: - now = time.monotonic() - if now > deadline: - print(f"\n[opencode] TOTAL TIMEOUT ({_TIMEOUT_MINUTES}min), killing process", flush=True) - proc.kill() - stop_reason = "total_timeout" - break - if now - last_output_time > _STALE_SECONDS: - print(f"\n[opencode] STALE TIMEOUT ({_STALE_SECONDS}s no output), killing process", flush=True) - proc.kill() - stop_reason = "stale_timeout" - break - continue - - if line is None: - break - - last_output_time = time.monotonic() - state.lines.append(line) - if raw_fh: - raw_fh.write(line) - _print_opencode_event(line, state) - if log_fh: - _log_opencode_event(line, state, log_fh) - finally: - if log_fh: - log_fh.close() - if raw_fh: - raw_fh.close() - if stderr_fh: - stderr_fh.close() - - try: - proc.wait(timeout=10) - except subprocess.TimeoutExpired: - proc.kill() - stop_reason = stop_reason or "total_timeout" - proc.wait(timeout=10) - - return state.lines, stop_reason - - -class _EventState: - def __init__(self) -> None: - self.lines: list[str] = [] - self._tool_by_call: dict[str, str] = {} - self._line_count: int = 0 - - -def _print_opencode_event(line: str, state: _EventState) -> None: - try: - ev = json.loads(line) - except json.JSONDecodeError: - return - - t = ev.get("type") - part = ev.get("part", {}) - - if t == "text": - text = part.get("text", "") - if text: - print(text, end="", flush=True) - state._line_count += text.count("\n") - - elif t == "tool_use": - tool = part.get("tool", "") - call_id = part.get("callID", "") - st = part.get("state", {}) - status = st.get("status", "") - inp = st.get("input", {}) - - if status == "pending": - state._tool_by_call[call_id] = tool - brief = json.dumps(inp, ensure_ascii=False)[:200] - print(f"\n > [AI: {tool}] {brief}", flush=True) - - elif status == "completed": - output = st.get("output", "") - if output: - display = output if len(output) <= 2000 else output[:2000] + "\n... [truncated]" - print(f"\n {'─' * 56}\n [AI output]\n {display}\n {'─' * 56}", flush=True) - - -def _log_opencode_event(line: str, state: _EventState, fh: Any) -> None: - try: - ev = json.loads(line) - except json.JSONDecodeError: - fh.write(line) - return - - t = ev.get("type") - part = ev.get("part", {}) - - if t == "text": - text = part.get("text", "") - if text: - fh.write(text) - - elif t == "tool_use": - tool = part.get("tool", "") - st = part.get("state", {}) - inp = json.dumps(st.get("input", {}), ensure_ascii=False) - fh.write(f"\n[AI: {tool}] <- {inp[:500]}\n") - output = st.get("output", "") - if output: - fh.write(f"{'─' * 60}\n[output]\n{output[:4000]}\n{'─' * 60}\n") - - fh.flush() - - -# ═══════════════════════════════════════════════════════════════════════════════ -# claude backend (streaming via `claude -p`) -# ═══════════════════════════════════════════════════════════════════════════════ -# -# Unlike proc.communicate() which blocks until the process exits (zero output -# in the meantime), this implementation streams stdout line-by-line in real -# time. A heartbeat "." is printed every 15s of silence to show liveness. - -def _run_claude(inputs: dict[str, Any]) -> AIResult: - """Run Claude Code with real-time streaming output. - - DESIGN NOTE — why we don't use proc.communicate(): - communicate() blocks until the process EXITS. This means ZERO output - is visible for up to 30 minutes, making it look like a hang. - Instead, we: - 1. Write the prompt to stdin in a background thread - 2. Read stdout line-by-line with a 1-second select() timeout - 3. Print each line immediately to the terminal - 4. Print a heartbeat "." every 15s of silence - 5. Kill the process if total timeout (30min) or stale timeout (5min - no output) is reached - """ - prompt = _build_prompt(inputs) - step_dir = inputs.get("step_dir", "") - step_path = Path(step_dir) if step_dir else None - log_path = step_path / "opencode.log" if step_path else None - stderr_path = step_path / "opencode_stderr.log" if step_path else None - - if log_path: - log_path.write_text("") - if stderr_path: - stderr_path.write_text("") - - _print_prompt(prompt, 0) - if log_path: - _log_prompt(prompt, 0, log_path) - - print(f"\n > [claude] Starting Claude Code (timeout={_TIMEOUT_MINUTES}min)...", flush=True) - print(f" (streaming output in real time — '.' = still thinking)", flush=True) - - # ── Launch claude ────────────────────────────────────────────────────── - stderr_fh = stderr_path.open("a", encoding="utf-8") if stderr_path else None - proc = subprocess.Popen( - ["claude", "-p", "--dangerously-skip-permissions"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=stderr_fh or subprocess.DEVNULL, - text=True, - bufsize=1, - env=_subprocess_env(), - ) - - # ── Write prompt to stdin in background thread ───────────────────────── - def _write_stdin(): - assert proc.stdin is not None - try: - proc.stdin.write(prompt) - proc.stdin.close() - except (BrokenPipeError, OSError): - pass - - stdin_thread = threading.Thread(target=_write_stdin, daemon=True) - stdin_thread.start() - - # ── Read stdout line-by-line in real time ────────────────────────────── - output_lines: list[str] = [] - log_fh = log_path.open("a", encoding="utf-8") if log_path else None - - deadline = time.monotonic() + _TIMEOUT_MINUTES * 60 - last_output_time = time.monotonic() - last_heartbeat = time.monotonic() - stop_reason: _StopReason | None = None - - try: - assert proc.stdout is not None - while True: - line = _read_line_with_timeout(proc.stdout, timeout=1.0) - - if line is None: - now = time.monotonic() - if now > deadline: - print(f"\n [claude] TOTAL TIMEOUT ({_TIMEOUT_MINUTES}min), killing process", flush=True) - proc.kill() - stop_reason = "total_timeout" - break - if now - last_output_time > _STALE_SECONDS: - print(f"\n [claude] STALE TIMEOUT ({_STALE_SECONDS}s no output), killing process", flush=True) - proc.kill() - stop_reason = "stale_timeout" - break - if now - last_heartbeat > _HEARTBEAT_INTERVAL: - print(".", end="", flush=True) - last_heartbeat = now - continue - - if line == "": - break - - last_output_time = time.monotonic() - last_heartbeat = time.monotonic() - - print(line, end="", flush=True) - output_lines.append(line) - - if log_fh: - log_fh.write(line) - finally: - if log_fh: - log_fh.close() - if stderr_fh: - stderr_fh.close() - - # ── Wait for process to finish ───────────────────────────────────────── - try: - proc.wait(timeout=10) - except subprocess.TimeoutExpired: - proc.kill() - stop_reason = stop_reason or "total_timeout" - proc.wait(timeout=10) - - stdout_data = "".join(output_lines) - - # ── Print exit status ────────────────────────────────────────────────── - if proc.returncode != 0: - print(f"\n [claude] exited with code {proc.returncode}", flush=True) - if stderr_path and stderr_path.exists(): - stderr_tail = stderr_path.read_text(encoding="utf-8", errors="replace")[-2000:] - if stderr_tail: - print(f" [claude] stderr tail:\n{stderr_tail}", flush=True) - else: - if last_heartbeat > last_output_time: - print(flush=True) - - if stop_reason: - print(f" [claude] Stopped due to: {stop_reason}", flush=True) - - return _build_result(step_path, inputs.get("ascend_path", ""), stdout_data) - - -def _read_line_with_timeout(stream: Any, timeout: float) -> str | None: - """Read a line from *stream* with a per-read *timeout* using select(). - - This is the key to non-blocking stdout reading. Without it, readline() - blocks until data arrives, preventing us from checking timeout/deadline - conditions. - - Returns: - A line string (with trailing newline) when data is available, - "" (empty string) on EOF, - None when *timeout* expires with no data available. - """ - ready, _, _ = select.select([stream], [], [], timeout) - if not ready: - return None - line = stream.readline() - return line # "" on EOF, "text\n" otherwise - - -# ═══════════════════════════════════════════════════════════════════════════════ -# shared result builder -# ═══════════════════════════════════════════════════════════════════════════════ - -def _build_result(step_dir: Path | None, ascend_path: str, output_text: str) -> AIResult: - """Build AIResult from AI output: extract summary, detect modified files.""" - summary = "" - if step_dir: - summary_path = step_dir / "step_summary.md" - if summary_path.exists(): - summary = summary_path.read_text(encoding="utf-8") - - if not summary: - summary = output_text[-4000:] if output_text else "" - - modified_files = _modified_files(ascend_path) - return AIResult( - modified_files=modified_files, - is_noop=not modified_files, - step_summary=summary, - ) - - -def _modified_files(ascend_path: str) -> list[str]: - if not ascend_path: - return [] - try: - result = subprocess.run( - ["git", "diff", "--name-only", "HEAD"], - cwd=ascend_path, - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError: - return [] - return [line for line in result.stdout.splitlines() if line] diff --git a/src/TA_main2main_workflow/agent/prompt.md b/src/TA_main2main_workflow/agent/prompt.md deleted file mode 100644 index 0211391..0000000 --- a/src/TA_main2main_workflow/agent/prompt.md +++ /dev/null @@ -1,489 +0,0 @@ -Resolve issues in the triton-ascend upstream sync for step {step_id}. -Previous step: {previous_step_id} -Previous step summary: {previous_step_summary_path} - -━━━ MISSION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -You are a single agent. Do NOT use TeamCreate or Agent tools — work -directly without sub-agents. - -Triton-Ascend is a fork of upstream Triton (triton-lang/triton) that adds -Ascend NPU support. - -The active mode is: {mode} - -── IR analysis modes (ir_analyze_ops / ir_analyze_changes / ir_generate_patch / ir_diagnose) ── - - If your mode starts with "ir_", you are performing LLVM IR compatibility - analysis. Your SOLE task is to analyze MLIR OP definitions, NOT merge - conflicts, NOT upstream Triton commits, NOT test failures. - - Your ONLY output is the structured JSON file specified in the mode-specific - instructions below. Do NOT produce analysis.md, step_summary.md, or - review.md. Do NOT analyze git merge history or upstream Triton commits. - -── conflict / fix / report / adapt modes ── - - If your mode is conflict, fix, report, or adapt, you are performing merge - conflict resolution and test fixing for the triton-ascend upstream sync. - After merging upstream changes via git merge, two types of issues may arise: - - 1. Merge conflicts — files with <<<<<<< / ======= / >>>>>>> markers - 2. Test failures — build errors or pytest failures caused by the merge - -── report mode ──────────────────────────────────────────────────── - - Trigger: {mode} is "report" (sync complete, generate summary). - - ALL data is in {error_logs} (JSON context file). Read it first. - - Generate a comprehensive report to {step_dir}/step_summary.md: - - ## 1. Executive Summary (总体概况) - - Upstream commits synced, steps, conflicts, build/test fixes, AI rounds - - ## 2. Per-Step Analysis (逐步分析) - - Commits merged, modules affected, conflicts and resolutions - - Build errors: root causes and fixes (specific files and error messages) - - Test failures: root causes and fixes (specific cases and fixes) - - ## 3. Fix Pattern Analysis (修复模式总结) - - Cross-step patterns, API changes, recurring issues - - Fixes that required multiple attempts - - ## 4. Recommendations (建议) - - Preventative measures, fragile areas - - Rules: DO NOT modify source code. Write in Chinese (中文). - Be specific with file paths, error messages, commit SHAs. - 用中文写同步工作流总结报告 - -── conflict mode ────────────────────────────────────────────────── - - Trigger: {mode} is "conflict" (merge conflicts exist). - - Workflow: - 1. Read {conflict_dir}/*.conflict files to see unresolved merge conflicts - 2. For each conflicted file, understand BOTH sides: - - The upstream triton change (incoming) - - The triton-ascend additions/modifications (current) - 3. Consult the in-depth reference docs (see README.md index) for the - conflict-resolution strategy BEFORE editing: - - {reference_dir}/01-merge-upstream-conflict-resolution.md — conflict - resolution strategy by file type, key case studies (Python frontend - refactor, BC pipeline, DotScale attribute rename), standard merge flow - - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md — when a - conflict involves IR/bytecode compatibility (BC pipeline, Op renames - like indirect→unstructured, AscendNPU-IR submodule updates) - - {reference_dir}/code-structure-guide.md — upstream → Ascend file mapping - 4. Resolve conflicts by: - - Keeping triton-ascend's Ascend-specific additions - - Accepting upstream triton changes that don't conflict with Ascend code - - When both sides modified the same code, integrate both changes - - Preserving Ascend-specific paths (python/triton_ascend/, third_party/ascend/) - 5. Check that resolved files are syntactically correct - 6. Write conflict resolution summary to {step_dir}/step_summary.md - 7. Stage resolved files with `git add ` for each resolved file - - Key principles for conflict resolution: - - Ascend-specific code (imports of triton_ascend, ascend device checks, - CANN/torch-npu references) must be preserved - - Upstream triton API changes should be accepted, but Ascend overrides - must be updated to match new signatures - - python/triton/ files are upstream code; changes here should follow - upstream unless they contain Ascend-specific modifications - - third_party/ascend/ files are entirely Ascend-specific; never overwrite - these with upstream changes - - lib/ and include/ changes should accept upstream C++ changes while - preserving Ascend backend registration code - -── fix mode ─────────────────────────────────────────────────────── - - Trigger: {mode} is "fix" (build or tests failed). - - Workflow: - 1. Read structured error output from {error_logs} - 2. Classify each failure: - - Build error → check include paths, missing symbols, CMake changes - - Import error → module path or symbol may have moved upstream - - Test failure → trace back to root cause in source code - - Environment flake → note but do not fix (timeout, network, resource) - 3. For each actionable failure, consult reference docs (see README.md index). - Always-useful quick guides: - - {reference_dir}/diagnosis-guide.md — error → root cause mapping - - {reference_dir}/error-pattern-examples.md — concrete fix patterns - - {reference_dir}/code-structure-guide.md — upstream → Ascend file mapping - Then read the in-depth guide matching the failure type: - - Build / compile errors (LLVM/MLIR API changes, CMake, undefined - symbols) → {reference_dir}/02-llvm-version-adaptation-and-compile-fixes.md - (LLVM/MLIR API change table, compat macros, LLVM patch mechanism) - - Unit-test / pytest failures (用例报错) → - {reference_dir}/03-unit-test-failure-diagnosis-and-fixes.md - (7 typical failure cases, API signature mismatches, pass-option - deprecations, post-upgrade test checklist) - - IR compatibility issues (BC pipeline, Op/IR structure changes, - NPUIR updates) → - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md - 4. Apply minimal fixes: - - Update imports when upstream moves modules - - Update function signatures when upstream changes APIs - - Update CMakeLists.txt when build configuration changes - - Fix pytest assertions when expected behavior changes - 5. Do NOT modify upstream triton code in python/triton/ unless it contains - Ascend-specific changes (marked with triton_ascend imports or ascend checks) - 6. Write fix summary to {step_dir}/step_summary.md - 7. Write a ONE-LINE commit message to {step_dir}/commit_message.txt - - Format: ": " - - Types: fix, build, test, cmake, compat - - Example: "fix: update AscendDotOp::build() signature for LLVM 22" - - Example: "test: fix pytest assertion for renamed attribute getLhs→getA" - - Keep under 72 characters, be specific about WHAT was fixed - - This line will be used as the git commit subject - - Common failure patterns in Triton-Ascend: - - python/triton/ changes → Ascend overrides in python/triton_ascend/ need updating - - lib/Target/ changes → Ascend backend in lib/Target/Ascend/ may need updating - - include/triton/ changes → Ascend headers may reference changed interfaces - - third_party/nvidia/ changes → Ascend third_party/ascend/ may need matching updates - - CMakeLists.txt changes → Ascend CMake configuration may need adjusting - -━━━ REPOSITORIES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - triton-ascend: {ascend_path} - upstream triton:{triton_path} - reference: {reference_dir} - -━━━ INPUTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - mode: {mode} - step: {step_id} - previous step: {previous_step_id} - previous step summary: {previous_step_summary_path} - error logs: {error_logs} - conflict directory: {conflict_dir} - archive directory: {step_dir} - upstream target: {target_commit} - -━━━ REFERENCE FILES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Start from the index, then open the doc that matches your task: - {reference_dir}/README.md — index of ALL adaptation docs - - Quick guides: - {reference_dir}/adapt-guide.md — adaptation workflow and decisions - {reference_dir}/code-structure-guide.md — Triton vs Triton-Ascend file mapping - {reference_dir}/diagnosis-guide.md — error type → root cause mapping - {reference_dir}/error-pattern-examples.md — concrete fix patterns per error type - - In-depth guides (from the 3.2→3.5 / LLVM 20→22 upgrade experience): - {reference_dir}/01-merge-upstream-conflict-resolution.md - — merge & conflict resolution: strategy by file type, key case studies - — USE FOR: resolving merge conflicts (conflict mode) - {reference_dir}/02-llvm-version-adaptation-and-compile-fixes.md - — LLVM/MLIR API change table, compat macros, LLVM patch mechanism - — USE FOR: fixing build / compilation errors (fix mode) - {reference_dir}/03-unit-test-failure-diagnosis-and-fixes.md - — 7 typical unit-test failure cases, post-upgrade test checklist - — USE FOR: fixing pytest / unit-test failures (fix mode, 用例报错) - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md - — BC pipeline, Op/IR structure changes, AscendNPU-IR submodule updates - — USE FOR: IR / bytecode compatibility issues (conflict or fix mode) - {reference_dir}/05-ir-patch-generation-guide.md - — direct OP patch strategy (TA-side only), patch format, OP change analysis - — USE FOR: generating LLVM backward-compatible OP patches (ir_generate_patch mode) - -━━━ RULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - Only modify files in {ascend_path} (triton-ascend repo) - - The upstream triton repo at {triton_path} is read-only for reference - - Do not run build commands, pip install, pytest, or CMake manually. - Build and test execution is handled externally by the main2main flow. - - Do not run git commit, git push, or git checkout. Only use `git add` to - stage resolved files in conflict mode. - - For conflict mode: the working tree has unmerged files. Resolve them in - place by editing the files to remove conflict markers. - - For fix mode: the working tree is clean (merge committed). Apply fixes - as new edits. - - Prefer minimal, targeted fixes over large refactors - - Preserve all Ascend-specific functionality (triton-ascend is the primary - codebase, not upstream triton) - - **NEVER modify code under `third_party/nvidia/` or `third_party/amd/`.** - These directories contain vendor-specific code that is NOT part of the - Ascend backend. Build errors or test failures in these paths must be - treated as environment issues, not code bugs — do not touch them. - Your fixes must be confined to Ascend-specific code paths: - - `third_party/ascend/` (Ascend backend implementation) - - `python/triton_ascend/` (Ascend Python bindings) - - `lib/Target/Ascend/` (Ascend LLVM backend) - - `python/triton/` (only if it contains Ascend conditionals) - - Other project files (CMakeLists.txt, setup.py, etc.) - - **NEVER keep triton-ascend's version of `cmake/llvm-hash.txt` in a merge - conflict.** This file must ALWAYS follow upstream triton. The LLVM version - is controlled by upstream; triton-ascend's LLVM patches are applied - separately and do NOT depend on a different LLVM hash. In any merge - conflict on this file, accept the upstream (incoming) version - unconditionally — do not preserve the triton-ascend side. - - When unsure about an upstream change's impact, search the triton-ascend - codebase for references to the changed symbol/file - -── ir_analyze_ops mode ──────────────────────────────────────────────── - - Trigger: {mode} is "ir_analyze_ops" (post-merge OP usage analysis). - - ⚠️ CRITICAL: This is NOT a merge analysis. Do NOT analyze upstream Triton - commits, merge conflicts, or test failures. Do NOT read {ascend_path}/.git - history. Your ONLY job is to scan Ascend backend source files for MLIR OP - usage and output the structured JSON report below. - - HINT: A pre-scan has been done — read {step_dir}/candidate_files.txt for - the list of files that contain MLIR OP patterns (::create, ::get, isa<, - cast<, etc.). Start from these files to find OP usages efficiently. - - Workflow: - 1. Read {step_dir}/candidate_files.txt for the list of files to scan. - 2. Scan each file for MLIR OP usage in these directories: - - `{ascend_path}/third_party/ascend/lib/` - - `{ascend_path}/lib/Target/Ascend/` - 2. For each OP, record: - - Fully qualified name (e.g., `triton::LoadOp`, `arith::AddIOp`) - - Source file and line number - - Usage type: create / match / transform - - Dialect it belongs to - - Its `assemblyFormat` string (if present) - 3. Output structured JSON to `{step_dir}/ops_report.json`: - {{ - "ops": [ - {{ - "name": "ascend::UnstructuredLoadOp", - "file": "lib/Target/Ascend/.../Ops.cpp", - "line": 42, - "usage": ["create", "match"], - "dialect": "ascend", - "assembly_format": "...", - "td_file": "mlir/include/mlir/Dialect/Ascend/IR/AscendOps.td" - }} - ], - "total_ops": 42, - "dialects": ["triton", "ascend", "arith", "scf", "linalg"] - }} - - Rules: DO NOT modify source code. Output ONLY the structured JSON report. - -── ir_analyze_changes mode ───────────────────────────────────────────── - - Trigger: {mode} is "ir_analyze_changes" (OP delta analysis between LLVM versions). - - ⚠️ CRITICAL: This is NOT a merge analysis. Do NOT analyze upstream Triton - commits, merge conflicts, or test failures. Do NOT read {ascend_path}/.git - history. Your ONLY job is to compare MLIR OP .td definitions between two - LLVM git commits and output the structured JSON report below. - - Context: - The Ascend backend OP usage is based on a fixed baseline LLVM version. - The target LLVM version is specified in cmake/llvm-hash.txt. OPs must - be checked for compatibility across these two versions. - - Baseline LLVM hash (source): {baseline_llvm_hash} - Target LLVM hash: {target_llvm_hash} - llvm-project repo: {llvm_project_path} - - ═══ HOW TO COMPARE — use git in the llvm-project repo ═══════════════════ - - For EVERY OP in ops_report.json, you MUST compare its .td definition at - the two LLVM versions using git. Do NOT guess or skip any OP. - - Step A — verify both commits exist: - cd {llvm_project_path} - git cat-file -t {baseline_llvm_hash} - git cat-file -t {target_llvm_hash} - - Step B — find the .td file for each OP: - Search for the OP's TableGen definition in mlir/include/: - grep -r "def " mlir/include/ --include="*.td" - - Step C — compare the definition at both versions: - git show {baseline_llvm_hash}:mlir/include/.../.td - git show {target_llvm_hash}:mlir/include/.../.td - Then diff the two definitions. - - Step D — also check for name changes (OP renamed): - git diff {baseline_llvm_hash}..{target_llvm_hash} -- mlir/include/ | grep "^[-+].*def " - This shows which OP definitions were added/removed between the two versions. - - Step E — for each OP, cross-reference with the Ascend backend usage: - Check how the OP is used in {ascend_path}/third_party/ascend/lib/ - and {ascend_path}/lib/Target/Ascend/ — does the OP use create(), - match(), or transformation patterns that depend on the old definition? - - ═══════════════════════════════════════════════════════════════════════════ - - Workflow: - 1. Read `{step_dir}/ops_report.json` for the list of OPs to check. - 2. For each OP, examine its TableGen (.td) definition in the llvm-project - at BOTH the baseline and target LLVM versions using the git commands above. - The llvm-project repo is at: {llvm_project_path} - Baseline (source): {baseline_llvm_hash} - Target (current): {target_llvm_hash} - 3. Record deltas per OP: - - Name change (old_name → new_name) - - assemblyFormat change (does the old format still parse?) - - create() / builder parameter signature change - - Attributes / getters renamed (e.g., getLhs → getA) - - Traits added/removed - - Custom printer/parser output format change - 4. Output to `{step_dir}/changes_report.json`: - {{ - "source_llvm_hash": "abc123", - "target_llvm_hash": "def456", - "changes": [ - {{ - "op_name": "arith::AddIOp", - "change_type": "assemblyFormat_changed", - "old_format": "...", - "new_format": "...", - "needs_patch": true, - "reason": "new LLVM generates IR in format old NPU-IR cannot parse" - }} - ], - "summary": {{ - "total_ops_analyzed": 42, - "ops_needing_patch": 5, - "renamed_ops": 1, - "signature_changes": 3 - }} - }} - - Reference: - {reference_dir}/02-llvm-version-adaptation-and-compile-fixes.md - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md - {reference_dir}/05-ir-patch-generation-guide.md - - Rules: DO NOT modify source code. Output ONLY the structured JSON report. - -── ir_generate_patch mode ────────────────────────────────────────────── - - Trigger: {mode} is "ir_generate_patch" (generate TA-side LLVM OP patches). - - Core strategy: patch TA-side LLVM so it generates IR compatible with the - UNMODIFIED AscendNPU-IR. NPU-IR is NOT touched — we cannot patch or - recompile it from the TA side. - - Workflow: - 1. Read `{step_dir}/changes_report.json` for ALL OPs needing patches. - 2. Read the patch template: - `{reference_dir}/ir_compatibility_patch_example.patch` - This demonstrates the direct OP patching approach (NOT BC/bytecode). - 3. Generate a SINGLE complete `.patch` file that covers ALL OPs flagged - with `needs_patch: true` in one unified patch. For each OP: - - Locate its .td / .cpp file in `{llvm_project_path}/mlir/` - - Apply the appropriate strategy by change type: - — OP renamed: add a backward-compatible alias (old name → new name) - — assemblyFormat changed: modify to also accept/emit old format - — create() params changed: add overload/defaults for old signature - — Pass option renamed: add old option name as alias - 4. Write the single patch to `{step_dir}/generated_patches/ir_compat.patch`: - - Follow `git format-patch` style with proper headers - - Apply cleanly to `{llvm_project_path}` as one atomic change - - Cover every OP in changes_report — do NOT leave any out - - Completeness requirement: the generated patch MUST be as complete as - possible. Missing even one OP will cause the outer loop to retry - (costly: LLVM rebuild takes ~2 hours). Review changes_report - thoroughly before writing the patch — every `needs_patch: true` OP - must have a corresponding fix in the patch. - - Reference: - {reference_dir}/05-ir-patch-generation-guide.md - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md - Template: {reference_dir}/ir_compatibility_patch_example.patch (single unified patch) - -── ir_diagnose mode ──────────────────────────────────────────────────── - - Trigger: {mode} is "ir_diagnose" (classify test failures as IR vs code). - - Workflow: - 1. Read test failure logs from {error_logs}. - 2. For each distinct failure, classify as: - a. "ir_compatibility" — LLVM/MLIR version mismatch: - - "unexpected op" / "custom op not registered" (OP renamed upstream) - - Missing dialect registration - - "attribute not found" for renamed properties - - assemblyFormat parse error (new IR format, old parser) - - triton-mlir-opt / bishengir-opt IR round-trip failures - b. "code_adaptation" — upstream API/signature change: - - Undefined symbols / missing includes - - Function signature mismatch - - Python ImportError / AttributeError / TypeError - - pytest assertion changes due to behavior changes - c. "environment" — non-code issue: - - Timeout, OOM, resource exhaustion - - Network failure, file not found (transient) - 3. Output to `{step_dir}/ir_diagnosis.json`: - {{ - "failures": [ - {{ - "id": "test_load_other", - "python_version": "3.10", - "error_summary": "...", - "classification": "ir_compatibility", - "affected_op": "triton::LoadOp", - "rationale": "..." - }} - ], - "summary": {{ - "total_failures": 5, - "ir_issues": 2, - "code_issues": 2, - "environment_issues": 1, - "has_ir_issues": true - }} - }} - - Reference: - {reference_dir}/03-unit-test-failure-diagnosis-and-fixes.md - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md - {reference_dir}/diagnosis-guide.md - {reference_dir}/error-pattern-examples.md - - Rules: DO NOT modify source code. Output ONLY the structured JSON diagnosis. - -━━━ OUTPUT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -── For ir_analyze_ops mode ── - Output ONLY `{step_dir}/ops_report.json` — the structured JSON specified - in the ir_analyze_ops section above. Do NOT write analysis.md, - step_summary.md, or review.md. Do NOT analyze merge history. - -── For ir_analyze_changes mode ── - Output ONLY `{step_dir}/changes_report.json` — the structured JSON specified - in the ir_analyze_changes section above. Do NOT write analysis.md, - step_summary.md, or review.md. Do NOT analyze merge history. - -── For ir_generate_patch mode ── - Output ONLY `{step_dir}/generated_patches/ir_compat.patch` — the unified - patch file specified in the ir_generate_patch section above. Do NOT write - analysis.md, step_summary.md, or review.md. - -── For ir_diagnose mode ── - Output ONLY `{step_dir}/ir_diagnosis.json` — the structured JSON specified - in the ir_diagnose section above. Do NOT write analysis.md, step_summary.md, - or review.md. - -── For conflict / fix / report / adapt modes ── - Archive all outputs to {step_dir}/: - - analysis.md — analysis of what upstream changes caused issues - step_summary.md — summary of resolutions/fixes applied - review.md — self-review of changes made - -For conflict mode, additionally output: - - Each resolved file path - - Rationale for how the conflict was resolved - -For fix mode, additionally output: - - Each failure and its root cause - - Fix applied and rationale - - Any failures that were intentionally not fixed (e.g., env flakes) - -After completing all work and writing archive files, stop. No final JSON -or extra summary output is required. diff --git a/src/TA_main2main_workflow/agent/prompt_build_fix.md b/src/TA_main2main_workflow/agent/prompt_build_fix.md new file mode 100644 index 0000000..936c0c8 --- /dev/null +++ b/src/TA_main2main_workflow/agent/prompt_build_fix.md @@ -0,0 +1,123 @@ +Fix build/compilation errors in the triton-ascend upstream sync for step {step_id}. + +--- +## MISSION +--- + +You are a single agent. Do NOT use TeamCreate or Agent tools — work +directly without sub-agents. + +Triton-Ascend is a fork of upstream Triton (triton-lang/triton) that adds +Ascend NPU support. + +Your task is to fix build/compilation errors caused by merging upstream +Triton changes. The working tree is clean (merge already committed). + +Workflow: + 1. Read build error logs from {error_logs} — these are file paths to + cmake/ninja/setup.py build output + 2. Classify each failure: + - Build error → check include paths, missing symbols, CMake changes + - LLVM/MLIR API change → update Ascend backend code to match new API + - Environment flake → note but do not fix (timeout, network, resource) + 3. For each actionable failure, consult reference docs (see README.md index). + Always-useful quick guides: + - {reference_dir}/diagnosis-guide.md — error → root cause mapping + - {reference_dir}/error-pattern-examples.md — concrete fix patterns + - {reference_dir}/code-structure-guide.md — upstream → Ascend file mapping + Then read the in-depth guide: + - {reference_dir}/02-llvm-version-adaptation-and-compile-fixes.md + (LLVM/MLIR API change table, compat macros, LLVM patch mechanism) + 4. Apply minimal fixes: + - Update imports when upstream moves modules + - Update function signatures when upstream changes APIs + - Update CMakeLists.txt when build configuration changes + - Fix include paths and missing symbol references + 5. Do NOT modify upstream triton code in python/triton/ unless it contains + Ascend-specific changes (marked with triton_ascend imports or ascend checks) + 6. Write fix summary to {step_dir}/step_summary.md + 7. Write a ONE-LINE commit message to {step_dir}/commit_message.txt + - Format: ": " + - Types: fix, build, cmake, compat + - Example: "fix: update AscendDotOp::build() signature for LLVM 22" + - Keep under 72 characters, be specific about WHAT was fixed + - This line will be used as the git commit subject + +Common failure patterns in Triton-Ascend: + - python/triton/ changes → Ascend overrides in python/triton_ascend/ need updating + - lib/Target/ changes → Ascend backend in lib/Target/Ascend/ may need updating + - include/triton/ changes → Ascend headers may reference changed interfaces + - third_party/nvidia/ changes → Ascend third_party/ascend/ may need matching updates + - CMakeLists.txt changes → Ascend CMake configuration may need adjusting + +--- +## REPOSITORIES +--- + + triton-ascend: {ascend_path} + upstream triton:{triton_path} + reference: {reference_dir} + +--- +## INPUTS +--- + + mode: build_fix + step: {step_id} + error logs: {error_logs} + archive directory: {step_dir} + upstream target: {target_commit} + +--- +## REFERENCE FILES +--- + + Start from the index, then open the doc that matches your task: + {reference_dir}/README.md — index of ALL adaptation docs + + Quick guides: + {reference_dir}/diagnosis-guide.md — error type → root cause mapping + {reference_dir}/error-pattern-examples.md — concrete fix patterns per error type + {reference_dir}/code-structure-guide.md — Triton vs Triton-Ascend file mapping + + In-depth guide: + {reference_dir}/02-llvm-version-adaptation-and-compile-fixes.md + — LLVM/MLIR API change table, compat macros, LLVM patch mechanism + +--- +## RULES +--- + + - Only modify files in {ascend_path} (triton-ascend repo) + - The upstream triton repo at {triton_path} is read-only for reference + - Do not run build commands, pip install, pytest, or CMake manually. + Build and test execution is handled externally by the main2main flow. + - Do not run git commit, git push, or git checkout. + - The working tree is clean (merge committed). Apply fixes as new edits. + - Prefer minimal, targeted fixes over large refactors + - Preserve all Ascend-specific functionality (triton-ascend is the primary + codebase, not upstream triton) + - **NEVER modify code under `third_party/nvidia/` or `third_party/amd/`.** + These directories contain vendor-specific code that is NOT part of the + Ascend backend. Build errors in these paths must be treated as + environment issues, not code bugs — do not touch them. + Your fixes must be confined to Ascend-specific code paths: + - `third_party/ascend/` (Ascend backend implementation) + - `python/triton_ascend/` (Ascend Python bindings) + - `lib/Target/Ascend/` (Ascend LLVM backend) + - `python/triton/` (only if it contains Ascend conditionals) + - Other project files (CMakeLists.txt, setup.py, etc.) + - When unsure about an upstream change's impact, search the triton-ascend + codebase for references to the changed symbol/file + +--- +## OUTPUT +--- + + Archive all outputs to {step_dir}/: + + step_summary.md — each failure and its root cause, fix applied and + rationale, any failures intentionally not fixed + commit_message.txt — ONE-LINE git commit subject (under 72 chars) + + After completing all work and writing archive files, stop. diff --git a/src/TA_main2main_workflow/agent/prompt_conflict.md b/src/TA_main2main_workflow/agent/prompt_conflict.md new file mode 100644 index 0000000..8b064de --- /dev/null +++ b/src/TA_main2main_workflow/agent/prompt_conflict.md @@ -0,0 +1,119 @@ +Resolve merge conflicts in the triton-ascend upstream sync for step {step_id}. + +--- +## MISSION +--- + +You are a single agent. Do NOT use TeamCreate or Agent tools — work +directly without sub-agents. + +Triton-Ascend is a fork of upstream Triton (triton-lang/triton) that adds +Ascend NPU support. + +Your task is to resolve merge conflicts caused by merging upstream Triton +changes into triton-ascend. Files with <<<<<<< / ======= / >>>>>>> markers +need to be resolved. + +Workflow: + 1. Read conflict files listed in {error_logs} to see unresolved merge conflicts + 2. For each conflicted file, understand BOTH sides: + - The upstream triton change (incoming) + - The triton-ascend additions/modifications (current) + 3. Consult the in-depth reference docs (see README.md index) for the + conflict-resolution strategy BEFORE editing: + - {reference_dir}/01-merge-upstream-conflict-resolution.md — conflict + resolution strategy by file type, key case studies (Python frontend + refactor, BC pipeline, DotScale attribute rename), standard merge flow + - {reference_dir}/04-ir-compatibility-and-backend-adaptation.md — when a + conflict involves IR/bytecode compatibility (BC pipeline, Op renames + like indirect→unstructured, AscendNPU-IR submodule updates) + - {reference_dir}/code-structure-guide.md — upstream → Ascend file mapping + 4. Resolve conflicts by: + - Keeping triton-ascend's Ascend-specific additions + - Accepting upstream triton changes that don't conflict with Ascend code + - When both sides modified the same code, integrate both changes + - Preserving Ascend-specific paths (python/triton_ascend/, third_party/ascend/) + 5. Check that resolved files are syntactically correct + 6. Write conflict resolution summary to {step_dir}/step_summary.md + 7. Stage resolved files with `git add ` for each resolved file + +Key principles for conflict resolution: + - Ascend-specific code (imports of triton_ascend, ascend device checks, + CANN/torch-npu references) must be preserved + - Upstream triton API changes should be accepted, but Ascend overrides + must be updated to match new signatures + - python/triton/ files are upstream code; changes here should follow + upstream unless they contain Ascend-specific modifications + - third_party/ascend/ files are entirely Ascend-specific; never overwrite + these with upstream changes + - lib/ and include/ changes should accept upstream C++ changes while + preserving Ascend backend registration code + +--- +## REPOSITORIES +--- + + triton-ascend: {ascend_path} + upstream triton:{triton_path} + reference: {reference_dir} + +--- +## INPUTS +--- + + mode: conflict + step: {step_id} + error logs: {error_logs} + conflict directory: {conflict_dir} + archive directory: {step_dir} + upstream target: {target_commit} + +--- +## REFERENCE FILES +--- + + Start from the index, then open the doc that matches your task: + {reference_dir}/README.md — index of ALL adaptation docs + + {reference_dir}/01-merge-upstream-conflict-resolution.md + — merge & conflict resolution: strategy by file type, key case studies + {reference_dir}/04-ir-compatibility-and-backend-adaptation.md + — BC pipeline, Op/IR structure changes, AscendNPU-IR submodule updates + {reference_dir}/code-structure-guide.md + — Triton vs Triton-Ascend file mapping + +--- +## RULES +--- + + - Only modify files in {ascend_path} (triton-ascend repo) + - The upstream triton repo at {triton_path} is read-only for reference + - Do not run build commands, pip install, pytest, or CMake manually. + Build and test execution is handled externally by the main2main flow. + - Do not run git commit, git push, or git checkout. Only use `git add` to + stage resolved files. + - The working tree has unmerged files. Resolve them in place by editing + the files to remove conflict markers. + - Prefer minimal, targeted fixes over large refactors + - Preserve all Ascend-specific functionality (triton-ascend is the primary + codebase, not upstream triton) + - **NEVER modify code under `third_party/nvidia/` or `third_party/amd/`.** + These directories contain vendor-specific code that is NOT part of the + Ascend backend. + - **NEVER keep triton-ascend's version of `cmake/llvm-hash.txt` in a merge + conflict.** This file must ALWAYS follow upstream triton. In any merge + conflict on this file, accept the upstream (incoming) version + unconditionally. + - When unsure about an upstream change's impact, search the triton-ascend + codebase for references to the changed symbol/file + +--- +## OUTPUT +--- + + Archive all outputs to {step_dir}/: + + step_summary.md — summary of resolutions applied, each resolved file + path and rationale for how the conflict was resolved + + After completing all work and writing archive files, stop. diff --git a/src/TA_main2main_workflow/agent/prompt_test_fix.md b/src/TA_main2main_workflow/agent/prompt_test_fix.md new file mode 100644 index 0000000..343bc77 --- /dev/null +++ b/src/TA_main2main_workflow/agent/prompt_test_fix.md @@ -0,0 +1,124 @@ +Fix test failures in the triton-ascend upstream sync for step {step_id}. + +--- +## MISSION +--- + +You are a single agent. Do NOT use TeamCreate or Agent tools — work +directly without sub-agents. + +Triton-Ascend is a fork of upstream Triton (triton-lang/triton) that adds +Ascend NPU support. + +Your task is to fix pytest/unit-test failures caused by merging upstream +Triton changes. The working tree is clean (merge and build already committed). + +Workflow: + 1. Read test failure logs from {error_logs} — these are file paths to + pytest-junit.xml and other test output + 2. Classify each failure: + - Import error → module path or symbol may have moved upstream + - Test failure → trace back to root cause in source code + - API signature mismatch → function/class interface changed upstream + - Assertion failure → expected behavior changed + - Environment flake → note but do not fix (timeout, network, resource) + 3. For each actionable failure, consult reference docs (see README.md index). + Always-useful quick guides: + - {reference_dir}/diagnosis-guide.md — error → root cause mapping + - {reference_dir}/error-pattern-examples.md — concrete fix patterns + - {reference_dir}/code-structure-guide.md — upstream → Ascend file mapping + Then read the in-depth guide: + - {reference_dir}/03-unit-test-failure-diagnosis-and-fixes.md + (7 typical failure cases, API signature mismatches, pass-option + deprecations, post-upgrade test checklist) + 4. Apply minimal fixes: + - Update imports when upstream moves modules + - Update function signatures when upstream changes APIs + - Fix pytest assertions when expected behavior changes + 5. Do NOT modify upstream triton code in python/triton/ unless it contains + Ascend-specific changes (marked with triton_ascend imports or ascend checks) + 6. Write fix summary to {step_dir}/step_summary.md + 7. Write a ONE-LINE commit message to {step_dir}/commit_message.txt + - Format: ": " + - Types: fix, test, compat + - Example: "test: fix pytest assertion for renamed attribute getLhs→getA" + - Keep under 72 characters, be specific about WHAT was fixed + - This line will be used as the git commit subject + +Common failure patterns in Triton-Ascend: + - python/triton/ changes → Ascend overrides in python/triton_ascend/ need updating + - include/triton/ changes → Ascend headers may reference changed interfaces + - Upstream API deprecations → Ascend code using old APIs needs updating + - Pytest assertion changes due to upstream behavior changes + +--- +## REPOSITORIES +--- + + triton-ascend: {ascend_path} + upstream triton:{triton_path} + reference: {reference_dir} + +--- +## INPUTS +--- + + mode: test_fix + step: {step_id} + error logs: {error_logs} + archive directory: {step_dir} + upstream target: {target_commit} + +--- +## REFERENCE FILES +--- + + Start from the index, then open the doc that matches your task: + {reference_dir}/README.md — index of ALL adaptation docs + + Quick guides: + {reference_dir}/diagnosis-guide.md — error type → root cause mapping + {reference_dir}/error-pattern-examples.md — concrete fix patterns per error type + {reference_dir}/code-structure-guide.md — Triton vs Triton-Ascend file mapping + + In-depth guide: + {reference_dir}/03-unit-test-failure-diagnosis-and-fixes.md + — 7 typical unit-test failure cases, post-upgrade test checklist + +--- +## RULES +--- + + - Only modify files in {ascend_path} (triton-ascend repo) + - The upstream triton repo at {triton_path} is read-only for reference + - Do not run build commands, pip install, pytest, or CMake manually. + Build and test execution is handled externally by the main2main flow. + - Do not run git commit, git push, or git checkout. + - The working tree is clean. Apply fixes as new edits. + - Prefer minimal, targeted fixes over large refactors + - Preserve all Ascend-specific functionality (triton-ascend is the primary + codebase, not upstream triton) + - **NEVER modify code under `third_party/nvidia/` or `third_party/amd/`.** + These directories contain vendor-specific code that is NOT part of the + Ascend backend. Test failures in these paths must be treated as + environment issues, not code bugs — do not touch them. + Your fixes must be confined to Ascend-specific code paths: + - `third_party/ascend/` (Ascend backend implementation) + - `python/triton_ascend/` (Ascend Python bindings) + - `lib/Target/Ascend/` (Ascend LLVM backend) + - `python/triton/` (only if it contains Ascend conditionals) + - Other project files (CMakeLists.txt, setup.py, etc.) + - When unsure about an upstream change's impact, search the triton-ascend + codebase for references to the changed symbol/file + +--- +## OUTPUT +--- + + Archive all outputs to {step_dir}/: + + step_summary.md — each failure and its root cause, fix applied and + rationale, any failures intentionally not fixed + commit_message.txt — ONE-LINE git commit subject (under 72 chars) + + After completing all work and writing archive files, stop. diff --git a/src/TA_main2main_workflow/flow.py b/src/TA_main2main_workflow/flow.py index 4523674..eefd82c 100644 --- a/src/TA_main2main_workflow/flow.py +++ b/src/TA_main2main_workflow/flow.py @@ -1,3852 +1,127 @@ -"""CrewAI Flow — Triton-Ascend main2main upstream sync (merge-based). +"""TA Main2Main Workflow — Triton-Ascend upstream sync orchestrator. -Node order: - initialize → detect_commits → execute_sync → push_to_github / handle_failure +Assembles pipeline steps:: -The flow uses a single orchestration node (execute_sync) that internally -runs merge → AI resolve conflicts → build → test → AI fix in a loop. -This avoids relying on CrewAI @listen → @listen signal chaining which -fails to propagate return values in some CrewAI versions. - -ALL progress is printed to the local console — no CrewAI web UI needed. -AI (opencode or claude) is invoked via subprocess for conflict resolution -and test fixing. + prepare → detect → plan → for each step: + merge → [resolve] → build⇄fix → test⇄fix → commit + → finalize → [push_pr] """ -import json -import os -import shutil -import subprocess -import time -from pathlib import Path -from typing import Literal - -from pydantic import BaseModel - -from crewai.flow import Flow, listen, start, router +from __future__ import annotations -from TA_main2main_workflow.agent.opencode_adapter import AIResult, run_opencode_adapter -from TA_main2main_workflow.scripts.build_test import build_triton_ascend, run_tests -from TA_main2main_workflow.scripts.detect_commits import detect -from TA_main2main_workflow.scripts.merge_upstream import run_merge, run_merge_incremental -from TA_main2main_workflow.scripts.plan_steps import run_plan -from TA_main2main_workflow.scripts.pre_ci_check import run_pre_ci_check, cleanup_temp_files -from TA_main2main_workflow.scripts.push_to_github import ( - push_and_create_pr, -) +from pathlib import Path +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.tracker import timed from TA_main2main_workflow.utils import ( - BUILD_LOG_FILE, BUILD_RESULT_FILE, CONFLICT_LOG_DIR, - EACH_STEP_SUMMARY_FILE, EACH_STEP_TARGET_PATCH_FILE, - FINAL_SUMMARY_FILE, FINAL_TARGET_PATCH_FILE, FIX_LOG_DIR, - HasNewCommits, HasNoNewCommits, - STEPS_DIR, STEPS_FILE, LINE_BUDGET, - TEST_RESULT_FILE, UpgradeCompleted, UpgradeFailed, - WORKSPACE_DIR, has_merge_conflicts, run_git, get_conflict_files, - commit_submodule, push_submodule, submodule_has_changes, - IR_ANALYSIS_DIR, IR_OPS_REPORT_FILE, - IR_CHANGES_REPORT_FILE, IR_DIAGNOSIS_FILE, IR_MAX_ITERATIONS, - ENV_SINGLE_STEP_MODE, ENV_BASE_BRANCH, get_base_branch_ref, LLVM_CHANGE_ANALYSIS_DIR, - print_header, print_section, print_step, print_status, print_info, - print_warn, print_error, print_key_value, - print_flow_progress, print_conflict_list, print_summary_table, - print_ai_call_info, print_ai_result, print_elapsed_total, - start_timer, stop_timer, + UpgradeCompleted, + UpgradeFailed, + WORKSPACE_DIR, ) - -_REFERENCE_DIR = str(Path(__file__).parent / "reference") - -# Baseline LLVM version that Ascend backend OP usage is built against. -# IR compatibility patches bridge from this version to the target LLVM. -_ASCEND_BASELINE_LLVM_HASH = "b5cc222d7429fe6f18c787f633d5262fac2e676f" - - -def _llvm_project_path() -> Path: - """Return the resolved llvm-project path (expands ~ and $HOME).""" - return Path(os.path.expanduser( - os.getenv("LLVM_PROJECT_PATH", "~/llvm-project"))) - - -def _llvm_install_prefix() -> Path: - """Return the resolved LLVM install prefix (expands ~ and $HOME).""" - return Path(os.path.expanduser( - os.getenv("LLVM_INSTALL_PREFIX_SYNC", "~/llvm-install-sync"))) - - -class TA_Main2MainState(BaseModel): - triton_ascend_path: str = "" - triton_path: str = "" - target_commit: str = "" - test_log_dir: str = "" - - merge_base: str = "" - ascend_head: str = "" - work_branch: str = "" - original_branch: str = "" - - upstream_commits_count: int = 0 - merge_has_conflicts: bool = False - conflict_files: list = [] - - build_passed: bool = False - test_passed: bool = False - - retry_count: int = 0 - max_retries: int = 10 - fix_errors: list = [] - - # ── Per-step tracking for sync report ── - build_fix_count: int = 0 # AI fix attempts for build failures - test_fix_count: int = 0 # AI fix attempts for test failures - conflict_files_resolved: int = 0 # Total merge conflicts resolved - step_details: list = [] # Per-step breakdown for report - fix_attempts: list = [] # Detailed fix attempt records - - final_status: str = "" - pr_url: str = "" - - llvm_prefix: str = "" - conda_env: str = "" - test_dir: str = "third_party/ascend/unittest/pytest_ut" - num_procs: int = 16 - - # ── Progressive step-by-step merge ── - steps: list = [] - total_steps: int = 0 - current_step: int = 0 - step_start_ascend_head: str = "" # ascend HEAD before current step - progressive_merge: bool = True - step_pr_descriptions: list = [] # accumulated step descriptions for PR body - - # ── IR Patch Loop State ── - ir_analysis_done: bool = False - ir_ops_report: dict = {} - ir_changes_report: dict = {} - ir_patches: list = [] - ir_patch_iteration: int = 0 - ir_max_iterations: int = 3 - ir_issues_found: int = 0 - ir_fix_count: int = 0 - llvm_hash_changed: bool = False - - # ── Pytest State ── - pytest_passed: bool = False - test_failures_by_python: dict = {} - ir_loop_details: list = [] - - summary_rows: list = [] - - -class TA_Main2MainFlow(Flow[TA_Main2MainState]): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - # ═══════════════════════════════════════════════════════════════════════════ - # Workspace info helper — prints paths, branches, and git status - # ═══════════════════════════════════════════════════════════════════════════ - - def _print_workspace_info(self, label: str = "") -> None: - """Print all relevant repo paths, current branches, and git status. - - Called at key workflow steps to provide full visibility into the - workspace state — which repos are in play, what branches they're on, - and whether there are uncommitted changes. - """ - header = f"Workspace Info{f' — {label}' if label else ''}" - print_section(header) - - # ── Resolve paths ── - llvm_proj = _llvm_project_path() - llvm_install = _llvm_install_prefix() - ascend_str = self.state.triton_ascend_path - triton_str = self.state.triton_path - - # ── Print all relevant paths ── - print_key_value("LLVM_PROJECT_PATH", str(llvm_proj)) - print_key_value("LLVM_INSTALL_PREFIX_SYNC", str(llvm_install)) - if self.state.llvm_prefix: - print_key_value("LLVM_INSTALL_PREFIX", self.state.llvm_prefix) - if ascend_str: - print_key_value("TRITON_ASCEND_PATH", ascend_str) - if triton_str: - print_key_value("TRITON_PATH", triton_str) - - # ── Print git branch + status for each repo ── - repos: list[tuple[str, Path]] = [] - if ascend_str: - ap = Path(ascend_str) - if ap.exists(): - repos.append(("triton-ascend", ap)) - if triton_str: - tp = Path(triton_str) - if tp.exists(): - # Skip triton if it's the same directory as triton-ascend - if not ascend_str or tp != Path(ascend_str): - repos.append(("triton", tp)) - if llvm_proj.exists(): - repos.append(("llvm-project", llvm_proj)) - - for repo_label, repo_path in repos: - try: - branch = run_git(repo_path, "branch", "--show-current").strip() - print_key_value(f"{repo_label} branch", branch) - status = run_git(repo_path, "status", "--porcelain").strip() - if status: - lines = status.splitlines() - print_info( - f"{repo_label} uncommitted changes ({len(lines)} files):" - ) - for line in lines[:10]: - print(f" {line}") - if len(lines) > 10: - print(f" ... and {len(lines) - 10} more") - else: - print_info(f"{repo_label} status: clean") - except Exception as e: - print_warn(f"Could not get git info for {repo_label}: {e}") - - # ═══════════════════════════════════════════════════════════════════════════ - # Mode dispatch — supports full (CrewAI), merge-only, and fix-only modes - # ═══════════════════════════════════════════════════════════════════════════ - - def kickoff(self, inputs: dict | None = None): - """Override CrewAI Flow.kickoff() to support TA_MODE dispatch. - - TA_MODE values: - full — Original CrewAI flow: merge → resolve → build → test → fix → PR - merge — Merge + AI resolve only, push work branch, skip build/test. - Used on ubuntu-latest CI to prepare the work branch before - NPU testing. - fix — AI fix on an existing work branch. Reads error logs from - TA_ERROR_LOGS_PATH, runs AI fix, commits & pushes. - """ - mode = os.getenv("TA_MODE", "full") - if os.getenv(ENV_SINGLE_STEP_MODE, "false").lower() == "true": - return self._run_single_step_mode(inputs) - elif mode == "merge": - return self._run_merge_mode(inputs) - elif mode == "fix": - return self._run_fix_mode(inputs) - else: - return super().kickoff(inputs=inputs) - - # ═══════════════════════════════════════════════════════════════════════════ - # Mode: merge — AI merge + resolve ONE step, then push work branch - # ═══════════════════════════════════════════════════════════════════════════ - - def _run_merge_mode(self, inputs: dict | None) -> str: - """Merge + AI-resolve for ONE progressive step. Push work branch, no build/test. - - Used in CI (ubuntu-latest) as the merge phase of the per-step pipeline. - Each call merges exactly one step's batch of upstream commits. The CI - workflow orchestrates the per-step loop: - - For each step N: - → ta-kickoff --mode=merge (merges step N, resolves conflicts, pushes) - → NPU build+test - → AI fix retries (if needed) - → advance to step N+1 - - Env vars: - TA_CURRENT_STEP — which step index to merge (0-based, default 0). - Step 0 does full init + detect + plan first. - Step N>0 resumes from an existing work branch. - """ - current_step = int(os.getenv("TA_CURRENT_STEP", "0")) - - # ── Apply inputs to state ── - if inputs: - for key, value in inputs.items(): - if hasattr(self.state, key): - setattr(self.state, key, value) - - self._print_workspace_info(f"Merge Mode — step {current_step}") - - # ── Force skip build/test in merge mode ── - os.environ["SKIP_BUILD"] = "true" - os.environ["SKIP_E2E_TEST"] = "true" - - ascend_path = Path(self.state.triton_ascend_path) - - if current_step == 0: - # ── First step: full init + detect + plan ── - self.initialize() - result = self.detect_commits() - if result == HasNoNewCommits: - print_info("No new commits — nothing to merge") - metadata_dir = WORKSPACE_DIR / "merge-metadata" - metadata_dir.mkdir(parents=True, exist_ok=True) - (metadata_dir / "no_changes.txt").write_text("true", encoding="utf-8") - self.state.summary_rows.append( - ("MERGE PHASE", "SKIP", "No new upstream commits") - ) - print_summary_table(self.state.summary_rows) - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - - # Store the plan for subsequent steps - self._write_step_plan() - else: - # ── Resume: checkout existing work branch ── - work_branch = os.getenv("TA_WORK_BRANCH", self.state.work_branch) - if not work_branch: - print_error("TA_WORK_BRANCH is required for TA_CURRENT_STEP > 0") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # Restore state from work branch metadata - self.state.work_branch = work_branch - self.state.triton_ascend_path = ( - self.state.triton_ascend_path - or os.getenv("TRITON_ASCEND_PATH") - or str(Path.cwd()) - ) - self.state.target_commit = ( - self.state.target_commit or os.getenv("TRITON_TARGET_COMMIT", "") - ) - - # Read step plan saved from step 0 - plan_file = WORKSPACE_DIR / "merge-metadata" / "step_plan.json" - if not plan_file.exists(): - print_warn("Step plan file not found — re-detecting commits") - # Lightweight re-init without full initialize - self.state.triton_ascend_path = self.state.triton_ascend_path or str(Path.cwd()) - self.state.triton_path = os.path.expanduser( - os.getenv("TRITON_PATH", self.state.triton_ascend_path)) - ascend_path = Path(self.state.triton_ascend_path) - # Fetch and checkout work branch - try: - run_git(ascend_path, "fetch", "origin", work_branch) - except Exception: - pass - run_git(ascend_path, "checkout", work_branch) - result = self.detect_commits() - if result == HasNoNewCommits: - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - self._write_step_plan() - else: - import json - plan_data = json.loads(plan_file.read_text(encoding="utf-8")) - self.state.total_steps = plan_data["total_steps"] - self.state.steps = plan_data["steps"] - self.state.upstream_commits_count = plan_data.get("upstream_commits_count", 0) - - # Minimal init for resume - self.state.triton_path = os.path.expanduser( - os.getenv("TRITON_PATH", str(ascend_path))) - # Fetch and checkout work branch - try: - run_git(ascend_path, "fetch", "origin", work_branch) - except Exception: - pass - run_git(ascend_path, "checkout", work_branch) - - # ── Validate step index ── - if current_step >= self.state.total_steps: - print_info(f"current_step={current_step} >= total_steps={self.state.total_steps} — " - f"all steps already merged") - metadata_dir = WORKSPACE_DIR / "merge-metadata" - metadata_dir.mkdir(parents=True, exist_ok=True) - (metadata_dir / "all_steps_done.txt").write_text("true", encoding="utf-8") - self._write_merge_metadata() - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - - step = self.state.steps[current_step] - step_id = step["id"] - is_last_step = (current_step == self.state.total_steps - 1) - self.state.current_step = current_step - self.state.retry_count = 0 - - print_header( - f"Step {current_step + 1}/{self.state.total_steps}: {step_id}" - ) - print_key_value("commits in step", str(step["commit_count"])) - print_key_value("end commit", step["end_commit"][:12]) - print_key_value("is last step", str(is_last_step)) - - # ── Work-branch guard ── - if current_step > 0 and self.state.work_branch: - current_branch = run_git(ascend_path, "branch", "--show-current").strip() - if current_branch != self.state.work_branch: - print_warn( - f"Expected work branch '{self.state.work_branch}' " - f"but on '{current_branch}' — switching" - ) - run_git(ascend_path, "checkout", self.state.work_branch) - - self.state.step_start_ascend_head = run_git( - ascend_path, "rev-parse", "HEAD" - ).strip() - - # ── Step A: git merge this step's commits ── - merge_result = self._do_step_merge(step) - if merge_result == UpgradeFailed: - self.state.final_status = UpgradeFailed - self._write_merge_metadata() - return UpgradeFailed - - # ── Step B: AI resolve conflicts ── - if self.state.merge_has_conflicts: - if not self._do_resolve_conflicts(): - self.state.final_status = UpgradeFailed - self._write_merge_metadata() - return UpgradeFailed - - # ── Step C: Skip build/test (NPU CI runs these) ── - print_header("Build & Test — Merge Mode") - print_info(f"Merge mode: deferring build/test for step {step_id} to NPU CI") - self.state.build_passed = True - self.state.test_passed = True - self.state.summary_rows.append(("Build", "DEFER", "Runs on NPU CI")) - self.state.summary_rows.append(("Tests", "DEFER", "Runs on NPU CI")) - - # ── Step D: Commit step merge progress ── - self._do_commit_step(step) - - # Record step description - desc = ( - f"✅ **{step_id}**: {step['commit_count']} commits, " - f"end_commit=`{step['end_commit'][:12]}`, " - f"source lines={step.get('source_changed_lines', '?')}" - ) - self.state.step_pr_descriptions.append(desc) - print_status(True, f"Step {step_id} merge committed") - - # ── Push work branch ── - self._push_work_branch_to_remote() - - # ── Write metadata for CI orchestration ── - self._write_merge_metadata() - - # Print summary - print_header(f"Merge Phase Complete — Step {step_id}") - print_key_value("Work branch", self.state.work_branch) - print_key_value("Current step", f"{current_step + 1}/{self.state.total_steps}") - print_key_value("Target commit", self.state.target_commit[:12]) - print_key_value("Is last step", str(is_last_step)) - print_info(f"Pushed to origin/{self.state.work_branch}") - if is_last_step: - print_info("This is the last step — PR will be created if tests pass") - else: - next_step_id = self.state.steps[current_step + 1]["id"] - print_info(f"Next: NPU tests on this step, then merge step {next_step_id}") - - self.state.summary_rows.append( - ("MERGE PHASE", "PASS", f"Step {step_id}, branch: {self.state.work_branch}") - ) - print_summary_table(self.state.summary_rows) - - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - - def _write_step_plan(self) -> None: - """Persist the step plan so subsequent merge-mode calls can resume.""" - import json - metadata_dir = WORKSPACE_DIR / "merge-metadata" - metadata_dir.mkdir(parents=True, exist_ok=True) - plan_data = { - "total_steps": self.state.total_steps, - "steps": self.state.steps, - "upstream_commits_count": self.state.upstream_commits_count, - } - (metadata_dir / "step_plan.json").write_text( - json.dumps(plan_data, indent=2, ensure_ascii=False), encoding="utf-8" - ) - print_info(f"Step plan saved: {self.state.total_steps} step(s)") - - def _push_work_branch_to_remote(self) -> None: - """Push the work branch to origin so NPU CI can access it.""" - ascend_path = Path(self.state.triton_ascend_path) - - # Check we're on the work branch - current = run_git(ascend_path, "branch", "--show-current").strip() - if current != self.state.work_branch: - run_git(ascend_path, "checkout", self.state.work_branch) - - # ── Configure git auth (same logic as push_to_github._ensure_gh_auth) ── - self._setup_git_auth_for_push(ascend_path) - - # ── Push AscendNPU-IR submodule first ── - self._push_submodule_if_needed() - - print_header("Push Work Branch") - try: - run_git(ascend_path, "push", "-u", "origin", self.state.work_branch) - print_status(True, f"Pushed {self.state.work_branch} to origin") - self.state.summary_rows.append( - ("Push branch", "PASS", self.state.work_branch) - ) - except Exception as e: - print_error(f"Failed to push work branch: {e}") - # Try with force if normal push fails (e.g., branch exists from prior run) - try: - print_warn("Retrying with --force...") - run_git( - ascend_path, "push", "-u", "--force", - "origin", self.state.work_branch, - ) - print_status(True, f"Force-pushed {self.state.work_branch}") - except Exception: - print_error("Force push also failed") - raise - - def _setup_git_auth_for_push(self, repo: Path) -> None: - """Configure git authentication for pushing to GitHub. - - 1. Login gh CLI explicitly against github.com (needed when git - remotes point to a proxy host that gh doesn't recognize). - 2. Run 'gh auth setup-git' to configure the git credential helper. - 3. Rewrite the origin URL to embed the token so git push works - even through url.insteadOf proxy rewriting. - """ - gh_token = os.getenv("GH_TOKEN", "") - if gh_token: - print_info("GH_TOKEN set — configuring git credential helper") - - # Explicit gh login against github.com — essential when the - # git remote points to a proxy host (gh needs to know about - # github.com independently of git remotes). - result = subprocess.run( - ["gh", "auth", "login", "--with-token", "--hostname", "github.com"], - input=gh_token + "\n", text=True, capture_output=True, - ) - if result.returncode == 0: - print_info("gh auth login --with-token: success") - else: - print_warn(f"gh auth login stderr: {result.stderr.strip()}") - - result = subprocess.run( - ["gh", "auth", "setup-git", "--hostname", "github.com"], - capture_output=True, text=True, - ) - if result.returncode == 0: - print_info("gh auth setup-git: success") - else: - print_warn(f"gh auth setup-git skipped " - f"(exit {result.returncode}): {result.stderr.strip()}") - # Rewrite origin URL to embed token (for git push through proxy) - try: - origin_url = run_git(repo, "remote", "get-url", "origin").strip() - if origin_url.startswith("https://"): - clean_url = origin_url.replace("https://", "", 1) - if "@" in clean_url: - clean_url = clean_url.split("@", 1)[1] - new_url = f"https://x-access-token:{gh_token}@{clean_url}" - run_git(repo, "remote", "set-url", "origin", new_url) - safe = f"https://x-access-token:***@{clean_url}" - print_info(f"origin URL rewritten with token: {safe}") - except Exception as exc: - print_warn(f"Could not rewrite origin URL: {exc}") - else: - # Verify gh CLI is authenticated (interactive or env-based) - try: - subprocess.run( - ["gh", "auth", "status"], - check=True, capture_output=True, text=True, - ) - subprocess.run( - ["gh", "auth", "setup-git"], - check=True, capture_output=True, text=True, - ) - print_info("Git credential helper configured via gh") - except subprocess.CalledProcessError as e: - print_error( - f"gh not authenticated and GH_TOKEN not set: {e.stderr.strip()}" - ) - raise RuntimeError( - "Cannot push to GitHub: no GH_TOKEN and gh CLI not authenticated. " - "Run 'gh auth login' locally or set GH_TOKEN in CI." - ) - - def _push_submodule_if_needed(self) -> None: - """Push AscendNPU-IR submodule changes to its remote. - - Raises RuntimeError on failure so the error is surfaced to - GitHub Actions and the workflow exits with code 1. - - Uses the same branch name as the parent repo's work branch so the - two repos stay in sync. Pushes with --force-with-lease to avoid - clobbering existing remote state. - """ - ascend_path = Path(self.state.triton_ascend_path) - if not push_submodule(ascend_path, self.state.work_branch): - raise RuntimeError( - f"Failed to push AscendNPU-IR submodule branch " - f"'{self.state.work_branch}'") - self.state.summary_rows.append( - ("Push AscendNPU-IR", "PASS", self.state.work_branch) - ) - - def _write_merge_metadata(self) -> None: - """Write work branch, target commit, and step progress for CI orchestration.""" - metadata_dir = WORKSPACE_DIR / "merge-metadata" - metadata_dir.mkdir(parents=True, exist_ok=True) - - (metadata_dir / "work_branch.txt").write_text( - self.state.work_branch, encoding="utf-8" - ) - (metadata_dir / "target_commit.txt").write_text( - self.state.target_commit, encoding="utf-8" - ) - (metadata_dir / "current_step.txt").write_text( - str(self.state.current_step), encoding="utf-8" - ) - (metadata_dir / "total_steps.txt").write_text( - str(self.state.total_steps), encoding="utf-8" - ) - is_last = (self.state.current_step >= self.state.total_steps - 1) - (metadata_dir / "is_last_step.txt").write_text( - str(is_last).lower(), encoding="utf-8" - ) - print_info(f"Metadata written to {metadata_dir} " - f"(step {self.state.current_step + 1}/{self.state.total_steps}, " - f"is_last={is_last})") - - # ═══════════════════════════════════════════════════════════════════════════ - # Mode: single-step — per-step merge → IR → build → test → fix - # ═══════════════════════════════════════════════════════════════════════════ - - def _run_single_step_mode(self, inputs: dict | None) -> str: - """Single-step mode: each planned step runs the full pipeline independently. - - For each step: - 1. Merge upstream commits + resolve conflicts - 2. If LLVM hash changed: IR analysis → patch → rebuild LLVM - 3. Build Triton-Ascend + AI fix compile errors - 4. Run tests + AI fix test failures - 5. Commit step progress - - After all steps: finalize + push + create PR. - - Controlled by TA_SINGLE_STEP_MODE=true env var. - """ - # ── Apply inputs to state ── - if inputs: - for key, value in inputs.items(): - if hasattr(self.state, key): - setattr(self.state, key, value) - - ascend_path = Path(self.state.triton_ascend_path) - - # ── Phase 0: Initialize ── - self.initialize() - - # ── Phase 1: Detect commits & plan steps ── - detect_result = self.detect_commits() - if detect_result == HasNoNewCommits: - print_info("No new commits — nothing to merge") - self.state.summary_rows.append( - ("Detect", "SKIP", "No new upstream commits")) - self.state.final_status = UpgradeCompleted +from TA_main2main_workflow.pipeline.prepare import prepare +from TA_main2main_workflow.pipeline.detect import run_detect +from TA_main2main_workflow.pipeline.plan import run_plan +from TA_main2main_workflow.pipeline.merge import merge_upstream_commit +from TA_main2main_workflow.pipeline.resolve import resolve_conflicts +from TA_main2main_workflow.pipeline.build import build +from TA_main2main_workflow.pipeline.test import test +from TA_main2main_workflow.pipeline.commit import commit_step +from TA_main2main_workflow.pipeline.finalize import finalize + +log = get_logger(__name__) + + +class TA_Main2MainFlow: + """Orchestrator — builds context, runs pipeline steps, handles PR.""" + + def __init__(self, config: TAConfig | None = None) -> None: + self.config = config or TAConfig.from_env() + + def run(self) -> str: + """Execute the full sync pipeline. Returns UpgradeCompleted or UpgradeFailed.""" + log.header("Triton-Ascend Upstream Sync") + log.key_value("AI Backend", self.config.ai_backend) + log.key_value("Max Retries", str(self.config.max_retries)) + + # ── Phase 0: Prepare workspace ──────────────────────────────────── + with timed("prepare"): + ctx = prepare(WorkflowContext(), self.config) + + # ── Phase 1: Detect ───────────────────────────────────────────── + log.header("Phase 1: Detect Upstream Commits") + with timed("detect"): + ctx = run_detect(ctx, self.config) + if not ctx.has_new_commits: + log.status(True, "Already up to date") return UpgradeCompleted - - print_header("Single-Step Mode — Per-Step Full Pipeline") - print_key_value("Total steps", str(self.state.total_steps)) - print_info("Each step: merge → [IR patch] → build → fix → test → fix → commit") - - # ── Phase 1.5: Build baseline LLVM (pre-merge, with Ascend patch) ── - if not self._build_baseline_llvm(): - print_error("Baseline LLVM build failed — cannot proceed") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Phase 2: Per-step loop ── - while self.state.current_step < self.state.total_steps: - step = self.state.steps[self.state.current_step] - step_id = step["id"] - self.state.retry_count = 0 - - print_header( - f"Single-Step {self.state.current_step + 1}/{self.state.total_steps}: {step_id}" - ) - print_key_value("commits in step", str(step["commit_count"])) - print_key_value("end commit", step["end_commit"][:12]) - reason = step.get("reason", "line_budget") - print_key_value("step reason", reason) - - self._print_workspace_info(f"Single-Step Mode — {step_id}") - - # Record ascend HEAD before this step - self.state.step_start_ascend_head = run_git( - ascend_path, "rev-parse", "HEAD" - ).strip() - - # ── Step A: git merge ── - merge_result = self._do_step_merge(step) - if merge_result == UpgradeFailed: - self._backup_code_state(f"failed-merge-{step_id}") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Step B: AI resolve conflicts ── - if self.state.merge_has_conflicts: - if not self._do_resolve_conflicts(): - self._backup_code_state(f"failed-conflict-{step_id}") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Step C: IR patch if LLVM hash changed in this step ── - if reason == "llvm_version": - print_section(f"LLVM Version Change in {step_id} — IR Patch Pipeline") - if not self._do_per_step_ir_patch(step): - self.state.final_status = UpgradeFailed + log.status(True, f"Found {ctx.upstream_commits_count} upstream commits") + + # ── Phase 2: Plan ─────────────────────────────────────────────── + log.header("Phase 2: Plan Steps") + with timed("plan"): + ctx = run_plan(ctx, self.config) + log.status(True, f"Planned {ctx.total_steps} step(s)") + + # ── Phase 3: Per-step loop ────────────────────────────────────── + while ctx.current_step < ctx.total_steps: + step = ctx.steps[ctx.current_step] + sid = step["id"] + log.header(f"Step {ctx.current_step + 1}/{ctx.total_steps}: {sid}") + log.key_value("commits", str(step["commit_count"])) + log.key_value("end commit", step["end_commit"][:12]) + ctx = ctx.copy_with(retry_count=0) + + with timed("merge"): + ctx = merge_upstream_commit(ctx, self.config) + if ctx.merge_has_conflicts: + log.status(False, f"Merge has {len(ctx.conflict_files)} conflict(s)") + else: + log.status(True, "Merge clean") + + if ctx.merge_has_conflicts: + with timed("resolve"): + ctx = resolve_conflicts(ctx, self.config) + if ctx.merge_has_conflicts: + log.error(f"Conflicts unresolved for {sid}") return UpgradeFailed + log.status(True, "Conflicts resolved") - # ── Step D: Build + AI fix compile errors ── - print_section(f"Build & Fix — {step_id}") - if not self._do_build_and_fix_loop(): - self._backup_code_state(f"failed-build-{step_id}") - self.state.final_status = UpgradeFailed + ctx = build(ctx, self.config) + if not ctx.build_passed: + log.error(f"Build failed for {sid}") return UpgradeFailed - # ── Step E: Test + AI fix test failures ── - if not self._do_test_and_fix_loop(): - self._backup_code_state(f"failed-test-{step_id}") - self.state.final_status = UpgradeFailed + ctx = test(ctx, self.config) + if not ctx.test_passed: + log.error(f"Tests failed for {sid}") return UpgradeFailed - # ── Step F: Commit step progress ── - self._do_commit_step(step) - - # Record step description for PR body - desc = ( - f"✅ **{step_id}**: {step['commit_count']} commits, " - f"end_commit=`{step['end_commit'][:12]}`, " - f"source lines={step.get('source_changed_lines', '?')}, " - f"reason={reason}" + ctx = commit_step(ctx, self.config) + ctx = ctx.copy_with(current_step=ctx.current_step + 1) + log.status( + True, f"Step {sid} completed ({ctx.current_step}/{ctx.total_steps})" ) - self.state.step_pr_descriptions.append(desc) - - # Record per-step detail for sync report - self.state.step_details.append({ - "step_id": step_id, - "step_index": self.state.current_step + 1, - "commits": step["commit_count"], - "end_commit": step["end_commit"][:12], - "source_lines": step.get("source_changed_lines", 0), - "conflict_files": len(self.state.conflict_files), - "build_fixes": self.state.build_fix_count, - "test_fixes": self.state.test_fix_count, - "retries": self.state.retry_count, - "reason": reason, - }) - - self.state.current_step += 1 - print_status(True, f"Step {step_id} completed " - f"({self.state.current_step}/{self.state.total_steps})") - - # ── Phase 3: Finalize ── - print_header("Finalize — Generate Summary & Push") - self._do_finalize() - # ── Phase 4: Push to GitHub + create PR ── - self.push_to_github() + # ── Phase 4: Finalize ─────────────────────────────────────────── + ctx = finalize(ctx) - self.state.summary_rows.append( - ("Single-Step Sync", "PASS", - f"{self.state.total_steps} step(s), branch: {self.state.work_branch}") - ) - print_summary_table(self.state.summary_rows) - print_elapsed_total() + if self.config.push_to_github: + self._push_pr(ctx) - self.state.final_status = UpgradeCompleted return UpgradeCompleted - # ═══════════════════════════════════════════════════════════════════════════ - # Mode: fix — AI fix on existing work branch - # ═══════════════════════════════════════════════════════════════════════════ - - def _run_fix_mode(self, inputs: dict | None) -> str: - """AI fix on an existing work branch. - - Reads error logs from TA_ERROR_LOGS_PATH, calls the AI fix engine - (_do_ai_fix), commits & pushes fixes. Used in CI after NPU tests fail. - """ - ascend_path_str = ( - (inputs or {}).get("triton_ascend_path") - or os.getenv("TRITON_ASCEND_PATH") - or str(Path.cwd()) - ) - work_branch = os.getenv("TA_WORK_BRANCH", "") - error_logs_path = os.getenv("TA_ERROR_LOGS_PATH", "") - attempt = int(os.getenv("TA_FIX_ATTEMPT", "1")) - target_commit = ( - (inputs or {}).get("target_commit") - or os.getenv("TRITON_TARGET_COMMIT", "") - ) - - if not work_branch: - print_error("TA_WORK_BRANCH is required for fix mode") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - ascend_path = Path(ascend_path_str) - - # ── Setup ── - print_header(f"Fix Mode — Attempt {attempt}") - print_key_value("work branch", work_branch) - print_key_value("error logs", error_logs_path or "") - print_key_value("target commit", target_commit[:12] if target_commit else "") - print_key_value("repo path", str(ascend_path)) - - self._print_workspace_info(f"Fix Mode — Attempt {attempt}") - - # Clean old workspace - if WORKSPACE_DIR.exists(): - shutil.rmtree(WORKSPACE_DIR) - WORKSPACE_DIR.mkdir(parents=True) - - # Populate minimal state - self.state.triton_ascend_path = str(ascend_path) - self.state.triton_path = os.getenv("TRITON_PATH", str(ascend_path)) - self.state.target_commit = target_commit - self.state.work_branch = work_branch - self.state.original_branch = work_branch - self.state.current_step = 0 - self.state.total_steps = 1 - self.state.steps = [{ - "index": 1, - "id": "fix-step-1", - "commit_count": 0, - "end_commit": target_commit or "", - "source_changed_lines": 0, - }] + def _push_pr(self, ctx: WorkflowContext) -> None: + from TA_main2main_workflow.pipeline.push_pr import push_and_create_pr - # ── Checkout work branch ── - print_section("Checkout Work Branch") try: - run_git(ascend_path, "fetch", "origin", work_branch) - except Exception as e: - print_warn(f"Could not fetch {work_branch}: {e}") - run_git(ascend_path, "checkout", work_branch) - print_status(True, f"Checked out {work_branch}") - - # Pull latest (in case previous fix attempts pushed) - try: - run_git(ascend_path, "pull", "origin", work_branch) - print_info("Pulled latest changes") - except Exception: - print_warn("Could not pull latest — continuing with local state") - - # ── Collect error logs ── - fix_errors: list[str] = [] - if error_logs_path: - error_path = Path(error_logs_path) - if error_path.exists(): - if error_path.is_dir(): - fix_errors = sorted( - str(p) for p in error_path.rglob("*") if p.is_file() - ) - print_info(f"Found {len(fix_errors)} error log file(s)") - else: - fix_errors = [str(error_path)] - print_info(f"Using error log: {error_path}") - - if not fix_errors: - print_warn("No error logs found — AI will analyze the codebase directly") - # Create a stub so _do_ai_fix has something to work with - stub_log = WORKSPACE_DIR / "no-error-logs.txt" - stub_log.write_text( - "No specific error logs were provided from the NPU CI run.\n" - "Please analyze the triton-ascend codebase for potential issues\n" - f"that could cause build or test failures after merging upstream triton.\n" - f"Target upstream commit: {target_commit}\n" - f"Work branch: {work_branch}\n" + pr_url = push_and_create_pr( + ascend_path=Path(ctx.triton_ascend_path), + github_repo=self.config.github_repo, + summary_path=WORKSPACE_DIR / "final_summary.md", + target_commit=ctx.target_commit, ) - fix_errors = [str(stub_log)] - - self.state.fix_errors = fix_errors - - # ── Set up step directory ── - step_dir = WORKSPACE_DIR / "fix-step-1" - step_dir.mkdir(parents=True, exist_ok=True) - - # ── Run AI fix ── - print_header("AI Fix Analysis") - print_info(f"Error sources ({len(fix_errors)}):") - for e in fix_errors[:10]: - print(f" • {e}") - if len(fix_errors) > 10: - print(f" ... and {len(fix_errors) - 10} more") - - try: - fix_ok = self._do_ai_fix(ascend_path, step_dir, attempt) + log.status(True, f"PR created: {pr_url}") except Exception as e: - print_error(f"AI fix crashed: {e}") - import traceback - traceback.print_exc() - fix_ok = False - - if not fix_ok: - print_error("AI fix did not produce any changes") - self.state.summary_rows.append( - ("AI fix", "FAIL", "No changes produced") - ) - print_summary_table(self.state.summary_rows) - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Commit and push ── - print_section("Commit & Push Fixes") - - # ── Commit submodule changes first ── - self.state.retry_count = attempt - 1 - self._commit_submodule_if_needed() - - # ── Clean temp artifacts BEFORE staging ── - cleanup_temp_files(ascend_path) - - status = run_git(ascend_path, "status", "--porcelain").strip() - if status: - run_git(ascend_path, "add", "-A") - staged = run_git(ascend_path, "diff", "--cached", "--name-only").strip() - if staged: - print_info(f"Files staged ({len(staged.splitlines())}):") - for f in staged.splitlines()[:10]: - print_info(f" - {f}") - commit_target = target_commit[:12] if target_commit else "upstream" - commit_msg = ( - f"[Sync](fix) AI-generated build/test failures fix " - f"for merging {commit_target}\n\n" - f"Upstream target: {commit_target}\n" - f"Fix attempt: {attempt}\n" - f"Work branch: {work_branch}\n" - ) - run_git(ascend_path, "commit", "-s", "-m", commit_msg) - print_status(True, "Committed AI fix") - - # ── Push AscendNPU-IR submodule first ── - self._push_submodule_if_needed() - - run_git(ascend_path, "push", "origin", work_branch) - print_status(True, f"Pushed to origin/{work_branch}") - self.state.summary_rows.append( - ("AI fix", "PASS", f"Attempt {attempt}") - ) - else: - print_info("No changes to commit after AI fix") - self.state.summary_rows.append( - ("AI fix", "NOOP", "No changes needed") - ) - - print_header("Fix Phase Complete!") - print_key_value("work branch", work_branch) - print_key_value("attempt", str(attempt)) - print_info(f"Next: re-trigger NPU tests on branch '{work_branch}'") - - print_summary_table(self.state.summary_rows) - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - - # ═══════════════════════════════════════════════════════════════════════════ - # Phase 0: Initialize - # ═══════════════════════════════════════════════════════════════════════════ - - @start() - def initialize(self): - start_timer("flow-total") - - print_header("Triton-Ascend Upstream Sync — Main2Main Flow") - print(f" Started: {time.strftime('%Y-%m-%d %H:%M:%S')}", flush=True) - print(f" AI Backend: {os.getenv('AI_BACKEND', 'auto-detect')}", flush=True) - print(f" Max Retries: {self.state.max_retries}", flush=True) - - if WORKSPACE_DIR.exists(): - shutil.rmtree(WORKSPACE_DIR) - WORKSPACE_DIR.mkdir(parents=True) - - raw_ascend = ( - self.state.triton_ascend_path - or os.getenv("TRITON_ASCEND_PATH") - or str(Path.cwd()) - ) - raw_triton = ( - self.state.triton_path - or os.getenv("TRITON_PATH") - or str(Path.cwd()) - ) - - self.state.triton_ascend_path = raw_ascend - self.state.triton_path = os.path.expanduser(raw_triton) - self.state.target_commit = ( - self.state.target_commit or os.getenv("TRITON_TARGET_COMMIT", "") - ) - self.state.llvm_prefix = os.getenv("LLVM_INSTALL_PREFIX", "") - self.state.conda_env = os.getenv("CONDA_ENV", "ta-upgrade") - self.state.num_procs = int(os.getenv("NUM_PROCS", "16")) - - if not self.state.test_log_dir: - self.state.test_log_dir = str(WORKSPACE_DIR / "test-logs") - - ascend_path = Path(self.state.triton_ascend_path) - - # ── safety: abort any stale merge ── - merge_head = ascend_path / ".git" / "MERGE_HEAD" - if merge_head.exists(): - print_warn("Found stale MERGE_HEAD from previous run, aborting it") - try: - run_git(ascend_path, "merge", "--abort") - print_info("Stale merge aborted successfully") - except Exception: - print_warn("merge --abort failed, trying reset --hard") - try: - run_git(ascend_path, "reset", "--hard", "HEAD") - except Exception: - pass - for stale in [".git/MERGE_MODE", ".git/MERGE_MSG", ".git/CHERRY_PICK_HEAD"]: - p = ascend_path / stale - if p.exists(): - p.unlink() - - ascend_branch = run_git(ascend_path, "branch", "--show-current").strip() - self.state.original_branch = ascend_branch or run_git( - ascend_path, "rev-parse", "HEAD" - ).strip() - - # ── Use the configured base branch for patch diffs ── - # The work branch is created from the base branch, so all diffs should - # be computed against it, not the checkout HEAD. - base_branch = os.getenv(ENV_BASE_BRANCH, "main") - base_ref = get_base_branch_ref() - try: - run_git(ascend_path, "fetch", "origin", base_branch) - except Exception: - print_warn(f"Could not fetch {base_ref}, using checkout HEAD as base") - try: - self.state.ascend_head = run_git( - ascend_path, "rev-parse", base_ref).strip() - except Exception: - self.state.ascend_head = run_git(ascend_path, "rev-parse", "HEAD").strip() - - print_section("Repository Configuration") - print_key_value("triton-ascend", self.state.triton_ascend_path) - print_key_value("upstream triton", self.state.triton_path) - print_key_value("target commit", self.state.target_commit or "") - print_key_value("original branch", self.state.original_branch) - print_key_value(f"base ({base_ref})", self.state.ascend_head[:12]) - - self._print_workspace_info("Phase 0: Initialize") - - self.state.summary_rows = [] - - # ═══════════════════════════════════════════════════════════════════════════ - # Phase 1: Detect upstream commits - # ═══════════════════════════════════════════════════════════════════════════ - - @router(initialize) - def detect_commits(self) -> Literal["HasNewCommits", "HasNoNewCommits"]: - start_timer("detect") - print_header("Phase 1: Detect Upstream Commits & Plan Steps") - - self._print_workspace_info("Phase 1: Detect Commits") - - ascend_path = Path(self.state.triton_ascend_path) - triton_path = Path(self.state.triton_path) - - result, has_new = detect( - ascend_path, - triton_path, - self.state.target_commit or None, - ) - - self.state.merge_base = result["merge_base"] - self.state.target_commit = result["target_commit"] - self.state.upstream_commits_count = result["upstream_commits_count"] - - print_key_value("merge_base", self.state.merge_base[:12]) - print_key_value("target", self.state.target_commit[:12]) - print_key_value("upstream commits", str(self.state.upstream_commits_count)) - print_key_value("changed files", str(result["changed_files_count"])) - print_key_value("changed lines", str(result["changed_lines"]["total"])) - - commits = result.get("upstream_commits", []) - if commits: - print_info(f"Commits to merge ({len(commits)}):") - for c in commits[:20]: - print(f" {c['sha'][:8]} {c['subject'][:80]}") - if len(commits) > 20: - print(f" ... and {len(commits) - 20} more") - - if not has_new: - print_status(True, "Already up to date — nothing to merge") - self.state.summary_rows.append(("Detect commits", "PASS", "No new commits")) - stop_timer("detect") - return HasNoNewCommits - - # ── Check if progressive merge is enabled ── - progressive_env = os.getenv("TA_PROGRESSIVE_MERGE", "true").lower() - self.state.progressive_merge = progressive_env != "false" - - # ── Plan steps: split commits into chunks based on line budget ── - if self.state.progressive_merge and self.state.upstream_commits_count > 1: - print_section("Step Planning") - line_budget = int(os.getenv("TA_LINE_BUDGET", str(LINE_BUDGET))) - print_key_value("line budget", str(line_budget)) - - plan = run_plan( - triton_path, - self.state.merge_base, - self.state.target_commit, - line_budget=line_budget, - ) - self.state.steps = plan["steps"] - self.state.total_steps = len(plan["steps"]) - - # ── Guard: if planner produced 0 steps (e.g., all commits filtered - # out), fall back to single-step mode so something still gets merged ── - if self.state.total_steps == 0: - print_warn("Plan returned 0 steps — falling back to single-step merge") - self.state.total_steps = 1 - self.state.steps = [{ - "index": 1, - "id": "step-1", - "commit_count": self.state.upstream_commits_count, - "start_commit": self.state.merge_base, - "end_commit": self.state.target_commit, - "source_changed_lines": result["changed_lines"]["total"], - }] - - print_status(True, f"Planned {self.state.total_steps} step(s) " - f"from {plan['total_source_commits']} source-touching commits " - f"({plan['total_commits']} total upstream commits)") - else: - # Single-step mode: treat everything as one step - self.state.total_steps = 1 - self.state.steps = [{ - "index": 1, - "id": "step-1", - "commit_count": self.state.upstream_commits_count, - "start_commit": self.state.merge_base, - "end_commit": self.state.target_commit, - "source_changed_lines": result["changed_lines"]["total"], - }] - if not self.state.progressive_merge: - print_info("TA_PROGRESSIVE_MERGE=false — using single-step mode") - else: - print_info("Only 1 upstream commit — using single-step mode") - - stop_timer("detect") - print_status(True, f"Found {self.state.upstream_commits_count} upstream commits to merge " - f"across {self.state.total_steps} step(s)") - self.state.summary_rows.append( - ("Detect commits", "PASS", - f"{self.state.upstream_commits_count} commits, {self.state.total_steps} step(s)") - ) - return HasNewCommits - - @listen(HasNoNewCommits) - def has_no_commits(self): - print_header("Sync Complete — Already Up To Date") - print_elapsed_total() - print_summary_table(self.state.summary_rows) - - # ═══════════════════════════════════════════════════════════════════════════ - # Phase 2: Execute Sync (orchestrates merge → resolve → build → test → fix) - # ═══════════════════════════════════════════════════════════════════════════ - # - # This is the core loop. It runs as a SINGLE @router node to avoid - # CrewAI @listen → @listen signal chaining issues. All sub-steps are - # internal method calls, not CrewAI routing targets. - - @router(detect_commits) - def execute_sync(self) -> Literal["UpgradeCompleted", "UpgradeFailed"]: - """Orchestrate the full sync pipeline — progressively or single-step. - - When progressive_merge is True (default), each planned step is merged - and validated independently before moving to the next. This keeps - AI conflict resolution and fix scopes small and manageable. - - The internal per-step call chain is: - _do_step_merge → _do_resolve_conflicts → _do_build_and_fix_loop → _do_commit_step → _push_step_progress - """ - try: - return self._execute_sync_inner() - except Exception as exc: - print_error(f"Unexpected error in execute_sync: {exc}") - import traceback - traceback.print_exc() - # Backup code before failing so partial work is preserved - self._backup_code_state(f"crash-step{self.state.current_step + 1}") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - def _execute_sync_inner(self) -> Literal["UpgradeCompleted", "UpgradeFailed"]: - """Inner body of execute_sync — wrapped by try/except for crash backup.""" - - # ── Iterate over each planned step ── - while self.state.current_step < self.state.total_steps: - step = self.state.steps[self.state.current_step] - step_id = step["id"] - self.state.retry_count = 0 - - print_header(f"Step {self.state.current_step + 1}/{self.state.total_steps}: {step_id}") - print_key_value("commits in step", str(step["commit_count"])) - print_key_value("end commit", step["end_commit"][:12]) - if "source_changed_lines" in step: - print_key_value("source lines", str(step["source_changed_lines"])) - - self._print_workspace_info(f"Phase 2: Execute Sync — {step_id}") - - ascend_path = Path(self.state.triton_ascend_path) - - # ── Work-branch guard: verify we're on the right branch ── - if self.state.current_step > 0 and self.state.work_branch: - current_branch = run_git(ascend_path, "branch", "--show-current").strip() - if current_branch != self.state.work_branch: - print_warn(f"Expected work branch '{self.state.work_branch}' " - f"but currently on '{current_branch}' — switching back") - run_git(ascend_path, "checkout", self.state.work_branch) - print_info(f"Same work branch: '{self.state.work_branch}' " - f"(step {self.state.current_step + 1}/{self.state.total_steps})") - - # Record ascend HEAD before this step (for per-step patch generation) - self.state.step_start_ascend_head = run_git( - ascend_path, "rev-parse", "HEAD" - ).strip() - - # ── Step A: git merge this step's end commit ── - result = self._do_step_merge(step) - if result == UpgradeFailed: - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Step B: AI resolve conflict (if merge had conflicts) ── - if self.state.merge_has_conflicts: - if not self._do_resolve_conflicts(): - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Step C: build → test → AI fix bug loop ── - try: - build_ok = self._do_build_and_fix_loop() - except Exception as exc: - print_error(f"_do_build_and_fix_loop crashed: {exc}") - import traceback - traceback.print_exc() - self.state.final_status = UpgradeFailed - return UpgradeFailed - - if not build_ok: - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Step D: commit step progress ── - self._do_commit_step(step) - - # ── Record step description for final PR body ── - desc = ( - f"✅ **{step_id}**: {step['commit_count']} commits, " - f"end_commit=`{step['end_commit'][:12]}`, " - f"source lines={step.get('source_changed_lines', '?')}" - ) - self.state.step_pr_descriptions.append(desc) - - # ── Record per-step detail for sync report ── - conflicts_in_step = len(self.state.conflict_files) - self.state.step_details.append({ - "step_id": step_id, - "step_index": self.state.current_step + 1, - "commits": step["commit_count"], - "end_commit": step["end_commit"][:12], - "source_lines": step.get("source_changed_lines", 0), - "conflict_files": conflicts_in_step, - "build_fixes": self.state.build_fix_count, - "test_fixes": self.state.test_fix_count, - "retries": self.state.retry_count, - }) - - # Move to next step - self.state.current_step += 1 - print_status(True, f"Step {step_id} completed successfully " - f"({self.state.current_step}/{self.state.total_steps})") - - # ── Phase 3+4: IR compatibility patches + pytest ut test ── - ir_ok = self._do_ir_patch_loop() - if not ir_ok: - print_error("IR patch loop did not converge — sync failed") - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Finalize: generate cumulative patch & summary ── - self._do_finalize() - self.state.final_status = UpgradeCompleted - return UpgradeCompleted - - # ═══════════════════════════════════════════════════════════════════════════ - # Internal step implementations - # ═══════════════════════════════════════════════════════════════════════════ - - def _do_step_merge(self, step: dict) -> Literal["HasNewCommits"] | Literal["UpgradeFailed"]: - """Merge this step's end_commit into triton-ascend. - - The first step creates a fresh work branch from upstream-ascend/main - and merges its end_commit. Subsequent steps merge their end_commit - on top of the SAME work branch — git handles the incremental merge - automatically by computing the diff between the previous end_commit - and the new one. - - ALL steps share ONE work branch. This is critical: we accumulate - changes on a single branch so the final PR contains the full history. - """ - start_timer("merge") - step_id = step["id"] - is_first_step = self.state.current_step == 0 - - ascend_path = Path(self.state.triton_ascend_path) - triton_path = Path(self.state.triton_path) - - # ── Verify / log work branch consistency ── - if is_first_step: - print_info(f"No work branch yet — will create one for step {step_id}") - else: - current_branch = run_git(ascend_path, "branch", "--show-current").strip() - if current_branch != self.state.work_branch: - print_warn(f"Expected work branch '{self.state.work_branch}' " - f"but currently on '{current_branch}' — switching back") - run_git(ascend_path, "checkout", self.state.work_branch) - print_info(f"Continuing on work branch: '{self.state.work_branch}' " - f"(verified same branch as step 1)") - - print_flow_progress("merge", f"[{step_id}] merging {step['end_commit'][:12]}") - - try: - if is_first_step: - # First step: create work branch and do full merge - merge_result = run_merge( - ascend_path, - triton_path, - step["end_commit"], - ) - self.state.work_branch = merge_result["work_branch"] - print_info(f"Created work branch: '{self.state.work_branch}' " - f"(all {self.state.total_steps} step(s) will use this branch)") - else: - # Subsequent step: merge on top of existing work branch - # fetch the new target if it's not already present - try: - run_git(ascend_path, "fetch", "upstream-triton", "--prune") - except Exception: - print_info("Could not fetch upstream-triton, assuming target is reachable") - - merge_result = run_merge_incremental( - ascend_path, - triton_path, - step["end_commit"], - self.state.work_branch, - ) - except Exception as exc: - print_error(f"Merge failed with exception: {exc}") - stop_timer("merge") - self.state.summary_rows.append( - (f"Merge step {step_id}", "FAIL", str(exc)[:60]) - ) - return UpgradeFailed - - self.state.merge_has_conflicts = merge_result["has_conflicts"] - self.state.conflict_files = merge_result.get("conflict_files", []) - - print_key_value("work branch", self.state.work_branch) - print_key_value("has conflicts", str(self.state.merge_has_conflicts)) - print_key_value("exit code", str(merge_result["merge_exit_code"])) - print_key_value("step", f"{self.state.current_step + 1}/{self.state.total_steps}") - - # If merge had non-zero exit but no conflict markers, that's a hard failure - if merge_result["merge_exit_code"] != 0 and not self.state.merge_has_conflicts: - print_error(f"Merge exited with code {merge_result['merge_exit_code']} " - f"but no conflict markers found — this is an unexpected failure") - stop_timer("merge") - self.state.summary_rows.append( - (f"Merge step {step_id}", "FAIL", - f"exit code {merge_result['merge_exit_code']}") - ) - return UpgradeFailed - - if self.state.merge_has_conflicts: - print_conflict_list(self.state.conflict_files) - stop_timer("merge") - self.state.summary_rows.append( - (f"Merge step {step_id}", "WARN", f"{len(self.state.conflict_files)} conflicts") - ) - else: - stop_timer("merge") - print_status(True, f"Step {step_id} merge succeeded with no conflicts") - self.state.summary_rows.append( - (f"Merge step {step_id}", "PASS", f"{step['commit_count']} commits") - ) - - return HasNewCommits - - def _do_resolve_conflicts(self) -> bool: - """AI-driven merge conflict resolution with retry loop. - - For each attempt (up to max_retries): - 1. Refresh the conflict file list from git - 2. Call opencode/claude with the conflict snapshots - 3. Check if all conflicts are resolved - 4. If not, retry with refreshed conflict list - - AI context includes: step index (N/total), is_last_step flag, - previous_step_id and previous_step_summary_path for continuity - (matching vllm-ascend's main2main_flow pattern). - - After all conflicts are resolved: - - git commit the resolution - - Run pre-CI checks (conflict markers, temp files, syntax) - - Write step summary and cumulative patch - - Returns True if all conflicts resolved, False otherwise. - """ - start_timer("resolve") - print_header("Phase 3: AI Conflict Resolution") - - ascend_path = Path(self.state.triton_ascend_path) - - step = self.state.steps[self.state.current_step] if self.state.steps else None - current_step_id = step["id"] if step else "step-0" - is_last_step = self.state.current_step == self.state.total_steps - 1 - - # Use step-specific directory in progressive mode, fall back to step-0 - if self.state.total_steps > 1 and self.state.steps: - step_dir = WORKSPACE_DIR / STEPS_DIR / current_step_id - else: - step_dir = WORKSPACE_DIR / "step-0" - step_dir.mkdir(parents=True, exist_ok=True) - - # ── Previous step context (matching vllm-ascend pattern) ── - previous_step = ( - self.state.steps[self.state.current_step - 1] - if self.state.current_step > 0 and self.state.steps else None - ) - previous_step_id = previous_step["id"] if previous_step else "" - previous_step_summary_path = ( - str(WORKSPACE_DIR / STEPS_DIR / previous_step_id / EACH_STEP_SUMMARY_FILE) - if previous_step_id else "" - ) - - conflict_dir = WORKSPACE_DIR / CONFLICT_LOG_DIR - - # AI resolve conflict: check if AI is disabled - if os.getenv("SKIP_AI_ANALYSIS", "false").lower() == "true": - print_warn("SKIP_AI_ANALYSIS=true — skipping AI conflict resolution!") - print_warn("Conflicts will NOT be resolved automatically.") - print_conflict_list(self.state.conflict_files) - print_info("To resolve: manually edit conflicted files, then run:") - print_info(f" cd {ascend_path} && git add -u && git commit --no-edit") - self.state.summary_rows.append(("AI resolve conflicts", "SKIP", "SKIP_AI_ANALYSIS set")) - return False - - # AI resolve conflict: detect backend (opencode / claude) - try: - from TA_main2main_workflow.agent.opencode_adapter import _detect_backend - backend = _detect_backend() - print_info(f"AI backend detected: {backend}") - except RuntimeError as e: - print_error(f"AI backend not available: {e}") - print_info("Install 'opencode' or 'claude' CLI, or set AI_BACKEND env var.") - self.state.summary_rows.append(("AI resolve conflicts", "FAIL", str(e)[:50])) - return False - - resolved_all = False - ai_result: AIResult | None = None - conflict_files = list(self.state.conflict_files) - original_conflict_count = len(conflict_files) - - # AI resolve conflict: retry loop (up to max_retries) - for attempt in range(1, self.state.max_retries + 1): - print_step(attempt, self.state.max_retries, "AI conflict resolution") - - conflict_files = get_conflict_files(ascend_path) - if not conflict_files: - print_status(True, "No conflicts detected — already resolved!") - resolved_all = True - break - - print_info(f"Files with conflicts: {len(conflict_files)}") - for f in conflict_files: - print(f" • {f}") - - print_ai_call_info( - backend=backend, - mode="conflict", - attempt=attempt, - max_attempts=self.state.max_retries, - ) - - # AI resolve conflict: invoke opencode/claude - # Context matches vllm-ascend pattern: is_last_step, - # previous_step_id, previous_step_summary_path, step index - try: - ai_result = run_opencode_adapter({ - "step_id": f"{current_step_id}-conflict-{attempt}", - "previous_step_id": previous_step_id, - "previous_step_summary_path": previous_step_summary_path, - "is_last_step": str(is_last_step).lower(), - "step_index": f"{self.state.current_step + 1}/{self.state.total_steps}", - "step_dir": str(step_dir), - "conflict_dir": str(conflict_dir), - "ascend_path": str(ascend_path), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "conflict", - "error_logs": json.dumps(conflict_files, ensure_ascii=False), - "target_commit": self.state.target_commit, - }) - except Exception as e: - print_error(f"AI call failed: {e}") - if attempt < self.state.max_retries: - print_info(f"Retrying... ({attempt}/{self.state.max_retries})") - continue - break - - if not has_merge_conflicts(ascend_path): - print_status(True, f"All conflicts resolved! (attempt {attempt})") - self.state.conflict_files_resolved += original_conflict_count - resolved_all = True - break - else: - still_conflicted = len(get_conflict_files(ascend_path)) - print_status(False, f"{still_conflicted} conflict(s) remain after attempt {attempt}") - conflict_files = get_conflict_files(ascend_path) - - if not resolved_all: - remaining = get_conflict_files(ascend_path) - print_error(f"Failed to resolve all conflicts after {self.state.max_retries} attempts") - print_conflict_list(remaining) - stop_timer("resolve") - self.state.summary_rows.append(("AI resolve conflicts", "FAIL", "Conflicts remain")) - return False - - # AI resolve conflict: git commit the resolution - # Clean temp artifacts first, then use git add -A to ensure - # AI-created files are NOT dropped. - cleanup_temp_files(ascend_path) - try: - run_git(ascend_path, "add", "-A") - staged = run_git(ascend_path, "diff", "--cached", "--name-only").strip() - if staged: - print_info(f"Files staged ({len(staged.splitlines())}):") - for f in staged.splitlines()[:10]: - print_info(f" - {f}") - run_git(ascend_path, "commit", "--no-edit", "-s") - print_status(True, "Committed conflict resolution") - except subprocess.CalledProcessError as e: - stderr = (e.stderr or "").strip() if hasattr(e, 'stderr') else str(e) - if "nothing to commit" in stderr.lower(): - print_info("Nothing to commit — resolution may already be committed") - else: - print_warn(f"Commit may have failed: {stderr[-200:]}") - - # pre-CI check: scan for leftover conflict markers, temp files, syntax errors - print_info("Running pre-CI check after conflict resolution...") - pre_ci_result = run_pre_ci_check(ascend_path, step_id="conflict-resolution") - if not pre_ci_result["all_passed"]: - print_warn("Pre-CI check found issues — review before proceeding") - self.state.summary_rows.append( - ("Pre-CI check", "PASS" if pre_ci_result["all_passed"] else "WARN", - f"{pre_ci_result.get('modified_files_count', 0)} files checked") - ) - - # ── Write step summary ── - summary_path = step_dir / EACH_STEP_SUMMARY_FILE - if ai_result and ai_result.step_summary and not summary_path.exists(): - summary_path.write_text(ai_result.step_summary, encoding="utf-8") - - # ── Generate step patch ── - try: - patch = run_git(ascend_path, "diff", self.state.ascend_head, "HEAD") - (step_dir / EACH_STEP_TARGET_PATCH_FILE).write_text(patch, encoding="utf-8") - except Exception: - pass - - stop_timer("resolve") - elapsed = ai_result.elapsed_seconds if ai_result else 0 - print_status(True, f"Conflict resolution complete ({elapsed:.0f}s AI time)") - self.state.summary_rows.append( - ("AI resolve conflicts", "PASS", f"{elapsed:.0f}s" if elapsed else "done") - ) - self.state.merge_has_conflicts = False - return True - - def _do_build_and_fix_loop(self) -> bool: - """build → AI fix compile-error loop (up to max_retries rounds). - - Only handles compilation errors. Tests are deferred to after all - upstream commits are merged and the final build passes. - """ - ascend_path = Path(self.state.triton_ascend_path) - step = self.state.steps[self.state.current_step] if self.state.steps else None - current_step_id = step["id"] if step else "step-0" - - # Use step-specific directory in progressive mode, fall back to step-0 - if self.state.total_steps > 1 and self.state.steps: - step_dir = WORKSPACE_DIR / STEPS_DIR / current_step_id - else: - step_dir = WORKSPACE_DIR / "step-0" - step_dir.mkdir(parents=True, exist_ok=True) - - build_passed = False - - for attempt in range(self.state.max_retries + 1): - is_fix_attempt = attempt > 0 - self.state.retry_count = attempt - - # AI fix compile errors (skip on first round) - if is_fix_attempt: - print_header(f"Fix Attempt {attempt}/{self.state.max_retries} (build)") - ai_ok = self._do_ai_fix(ascend_path, step_dir, attempt) - # Collect fix detail - modified_files: list[str] = [] - ai_summary = "" - if hasattr(self, '_last_ai_result') and self._last_ai_result: - modified_files = self._last_ai_result.get("modified_files", []) - ai_summary = self._last_ai_result.get("step_summary", "") - # Read error log snippet for context - error_snippet = "" - for err_path in self.state.fix_errors: - try: - content = Path(err_path).read_text(encoding="utf-8", errors="replace") - error_snippet += content[-2000:] if len(content) > 2000 else content - except Exception: - pass - self.state.fix_attempts.append({ - "step_id": current_step_id, - "attempt": attempt, - "fix_type": "build", - "error_logs": list(self.state.fix_errors), - "error_snippet": error_snippet[-1500:], - "modified_files": modified_files, - "ai_summary": (ai_summary or "")[:2000], - "ai_ok": ai_ok, - }) - if not ai_ok: - pass - - # build triton-ascend - if not self._do_build(ascend_path, clean=(attempt == 0)): - if os.getenv("SKIP_AI_ANALYSIS", "false").lower() == "true": - return False - self.state.fix_errors = [str(WORKSPACE_DIR / BUILD_RESULT_FILE)] - self.state.build_fix_count += 1 - print_warn(f"Build failed (attempt {attempt + 1}/{self.state.max_retries + 1}) — " - f"will retry after AI fix") - print_info(f"Build log: {WORKSPACE_DIR / BUILD_LOG_FILE}") - continue - - # Build passed — tests are deferred to after all merges complete - build_passed = True - break - - if not build_passed: - print_error(f"All {self.state.max_retries} fix attempts exhausted — build still failing") - self.state.summary_rows.append( - ("AI fix", "FAIL", f"Failed after {self.state.max_retries} attempts") - ) - return False - - # Commit build fixes - self._commit_fixes(ascend_path, step_dir) - - return True - - def _commit_submodule_if_needed(self) -> None: - """Commit uncommitted changes inside the AscendNPU-IR submodule. - - Must be called BEFORE parent 'git add -A' so that the submodule - pointer update is picked up by the parent commit. - """ - ascend_path = Path(self.state.triton_ascend_path) - if not submodule_has_changes(ascend_path): - return - - target_short = self.state.target_commit[:12] - commit_msg = ( - f"[Sync](fix) AI-generated build/test failures fix " - f"for merging {target_short}\n\n" - f"Upstream target: {target_short}\n" - f"Fix attempt: {self.state.retry_count}\n" - f"Work branch: {self.state.work_branch}\n" - ) - commit_submodule(ascend_path, commit_msg) - - def _commit_fixes(self, ascend_path: Path, step_dir: Path) -> None: - """Commit AI bug fixes with a meaningful message. - - Only commits if there are uncommitted changes. Commits submodule - changes first (AscendNPU-IR), then returns to triton-ascend for the - parent commit. Uses git add -A so AI-created files are not dropped. - - Commit message priority: - 1. AI-written commit_message.txt (one-line subject) - 2. First line of step_summary.md - 3. Default generic message - """ - # ── Commit submodule changes first (inside AscendNPU-IR) ── - self._commit_submodule_if_needed() - - # ── Clean temp artifacts BEFORE staging ── - # Clean first, then check status — otherwise temp files that - # AI fixes didn't touch would cause a false-positive "need to commit". - cleanup_temp_files(ascend_path) - - status = run_git(ascend_path, "status", "--porcelain").strip() - if not status: - print_info("No uncommitted fix changes — nothing to commit") - return - - print_section("Commit Bug Fixes") - - target_short = self.state.target_commit[:12] - - # ── Read AI-written commit message ── - commit_msg_path = step_dir / "commit_message.txt" - if commit_msg_path.exists(): - commit_summary = commit_msg_path.read_text(encoding="utf-8").strip() - # Take first line only for the subject - commit_summary = commit_summary.split("\n")[0].strip()[:72] - print_info(f"Using AI-written commit message: {commit_summary}") - else: - # Fallback: first line of step_summary.md - summary_path = step_dir / EACH_STEP_SUMMARY_FILE - if summary_path.exists(): - summary_text = summary_path.read_text(encoding="utf-8").strip() - commit_summary = summary_text.split("\n")[0].lstrip("#").strip()[:72] - else: - commit_summary = f"Resolve build/test failures for merging {target_short}" - commit_msg = ( - f"[Sync](fix) {commit_summary}\n\n" - f"Upstream target: {target_short}\n" - f"Fix attempt: {self.state.retry_count}\n" - f"Work branch: {self.state.work_branch}\n" - f"Co-Authored-By: Claude \n" - ) - - # ── Stage and commit (already changed to -A above via replace_all) ── - try: - staged_before = run_git(ascend_path, "diff", "--cached", "--name-only").strip() - if not staged_before: - # git add -A was already called; if no files staged yet, stage now - run_git(ascend_path, "add", "-A") - staged = run_git(ascend_path, "diff", "--cached", "--name-only").strip() - if staged: - print_info(f"Files staged for commit ({len(staged.splitlines())}):") - for f in staged.splitlines()[:15]: - print_info(f" - {f}") - if len(staged.splitlines()) > 15: - print_info(f" ... and {len(staged.splitlines()) - 15} more") - run_git(ascend_path, "commit", "-s", "-m", commit_msg) - print_status(True, f"Committed fix: {commit_summary[:60]}") - self.state.summary_rows.append(("Commit fixes", "PASS", commit_summary[:40])) - except subprocess.CalledProcessError as e: - stderr = (e.stderr or "").strip() if hasattr(e, 'stderr') else str(e) - if "nothing to commit" in stderr.lower(): - print_info("Nothing to commit (AI made no changes)") - self.state.summary_rows.append(("Commit fixes", "PASS", "No changes")) - else: - print_warn(f"Could not commit fixes: {stderr[-200:]}") - self.state.summary_rows.append(("Commit fixes", "WARN", stderr[:40])) - - def _do_build(self, ascend_path: Path, clean: bool = False, - python_exe: str = "python3") -> bool: - start_timer("build") - print_section("Build Triton-Ascend") - - if os.getenv("SKIP_BUILD", "false").lower() == "true": - print_info("SKIP_BUILD=true — skipping build") - self.state.build_passed = True - stop_timer("build") - self.state.summary_rows.append(("Build", "SKIP", "SKIP_BUILD set")) - return True - - build_result = build_triton_ascend( - ascend_path, - llvm_prefix=self.state.llvm_prefix, - conda_env=self.state.conda_env, - clean_build=clean, - python_exe=python_exe, - ) - self.state.build_passed = build_result["all_passed"] - stop_timer("build") - - if not self.state.build_passed: - print_error("Build FAILED") - self.state.summary_rows.append(("Build", "FAIL", "See build log")) - return False - - print_status(True, "Build passed") - self.state.summary_rows.append(("Build", "PASS", "")) - return True - - def _do_test(self, ascend_path: Path, python_exe: str = "") -> bool | None: - start_timer("test") - print_section("Run Tests") - - if os.getenv("SKIP_E2E_TEST", "false").lower() == "true": - print_info("SKIP_E2E_TEST=true — treating tests as passed") - self.state.test_passed = True - stop_timer("test") - self.state.summary_rows.append(("Tests", "SKIP", "SKIP_E2E_TEST set")) - return None - - test_dir_path = ascend_path / self.state.test_dir - py_label = python_exe or os.getenv("PYTHON", "python3.10") - print_info(f"Test directory: {test_dir_path}") - print_info(f"Python: {py_label}, procs: {self.state.num_procs}") - - try: - test_result = run_tests( - ascend_path, - test_dir=self.state.test_dir, - num_procs=self.state.num_procs, - conda_env=self.state.conda_env, - python_exe=python_exe, - ) - except Exception as exc: - print_error(f"run_tests raised exception: {exc}") - import traceback - traceback.print_exc() - self.state.test_passed = False - stop_timer("test") - self.state.summary_rows.append(("Tests", "FAIL", f"Exception: {exc}")) - return False - - self.state.test_passed = test_result["passed"] - stop_timer("test") - - if test_result["passed"]: - passed_count = test_result.get("passed_count", "?") - print_status(True, f"All tests passed ({passed_count} passed)") - self.state.summary_rows.append(("Tests", "PASS", f"{passed_count} passed")) - return True - else: - failed_count = test_result.get("failed_count", "?") - error_count = test_result.get("error_count", 0) - error_msg = test_result.get("error", "") - if error_msg: - print_error(f"Tests FAILED — {error_msg}") - else: - print_error(f"Tests FAILED ({failed_count} failed, {error_count} errors)") - self.state.summary_rows.append( - ("Tests", "FAIL", f"{failed_count} failed, {error_count} errors") - ) - return False - - def _do_ai_fix(self, ascend_path: Path, step_dir: Path, attempt: int) -> bool: - """AI fix bug: invoke opencode/claude to fix build/test failures. - - AI context includes: step index, is_last_step, previous_step_summary - (matching vllm-ascend's main2main_flow pattern). - """ - print_step(attempt, self.state.max_retries, "AI fix attempt") - - step = self.state.steps[self.state.current_step] if self.state.steps else None - current_step_id = step["id"] if step else "step-0" - is_last_step = self.state.current_step == self.state.total_steps - 1 - - # ── Previous step context (matching vllm-ascend pattern) ── - previous_step = ( - self.state.steps[self.state.current_step - 1] - if self.state.current_step > 0 and self.state.steps else None - ) - previous_step_id = previous_step["id"] if previous_step else "" - previous_step_summary_path = ( - str(WORKSPACE_DIR / STEPS_DIR / previous_step_id / EACH_STEP_SUMMARY_FILE) - if previous_step_id else "" - ) - - # Per-attempt fix directory for logs/artifacts. The step_dir is the - # canonical per-step directory (matching vllm-ascend pattern). - fix_dir = WORKSPACE_DIR / FIX_LOG_DIR / f"{current_step_id}-fix-{attempt}" - fix_dir.mkdir(parents=True, exist_ok=True) - - print_info(f"Error sources ({len(self.state.fix_errors)}):") - for e in self.state.fix_errors: - print(f" • {e}") - - # AI fix bug: detect backend - try: - from TA_main2main_workflow.agent.opencode_adapter import _detect_backend - backend = _detect_backend() - except RuntimeError as e: - print_error(f"AI backend not available: {e}") - self._last_ai_result = None - return False - - print_ai_call_info( - backend=backend, - mode="fix", - attempt=attempt, - max_attempts=self.state.max_retries, - ) - - # AI fix bug: invoke opencode/claude with error logs - # Context matches vllm-ascend pattern: is_last_step, - # previous_step_id, previous_step_summary_path, step index. - # step_dir points to the canonical step directory (like vllm-ascend); - # fix_dir captures per-attempt fix artifacts separately. - error_logs = json.dumps(self.state.fix_errors, ensure_ascii=False) - try: - ai_result = run_opencode_adapter({ - "step_id": f"{current_step_id}-fix-{attempt}", - "previous_step_id": previous_step_id, - "previous_step_summary_path": previous_step_summary_path, - "is_last_step": str(is_last_step).lower(), - "step_index": f"{self.state.current_step + 1}/{self.state.total_steps}", - "step_dir": str(step_dir), - "fix_dir": str(fix_dir), - "conflict_dir": "", - "ascend_path": str(ascend_path), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "fix", - "error_logs": error_logs, - "target_commit": self.state.target_commit, - }) - - print_ai_result( - ok=bool(ai_result.modified_files), - modified_files=ai_result.modified_files, - summary=(ai_result.step_summary or "")[:500], - ) - - # Store result for caller to capture fix details - self._last_ai_result = { - "modified_files": ai_result.modified_files, - "step_summary": ai_result.step_summary or "", - "is_noop": ai_result.is_noop, - "elapsed_seconds": ai_result.elapsed_seconds, - } - - print_info("Running pre-CI check after fix...") - run_pre_ci_check(ascend_path, step_id=f"fix-{attempt}") - - return bool(ai_result.modified_files) - - except Exception as e: - print_error(f"AI fix call failed: {e}") - self._last_ai_result = None - return False - - def _do_commit_step(self, step: dict) -> None: - """Commit the current step's progress with a descriptive message. - - Only commits if there are uncommitted changes. Uses "git add -u" to - avoid staging test artifacts or transient files. - - Commits AscendNPU-IR submodule changes first (if any), so the parent - repo records the updated submodule pointer. - """ - ascend_path = Path(self.state.triton_ascend_path) - step_id = step["id"] - - # ── Commit submodule changes first ── - self._commit_submodule_if_needed() - - status = run_git(ascend_path, "status", "--porcelain").strip() - - if not status: - print_info(f"[{step_id}] No uncommitted changes — nothing to commit") - self.state.summary_rows.append( - (f"Commit {step_id}", "PASS", "No changes (clean merge)") - ) - return - - print_section(f"Commit Step {step_id}") - - # Clean up temp artifacts before staging to avoid committing them - cleanup_temp_files(ascend_path) - - end_commit_short = step["end_commit"][:12] - commit_msg = ( - f"sync: merge upstream commits for step {step_id}\n\n" - f"Upstream range: {step.get('start_commit', '?')[:12]}..{end_commit_short}\n" - f"Step: {self.state.current_step + 1}/{self.state.total_steps}\n" - f"Commits in step: {step['commit_count']}\n" - f"Work branch: {self.state.work_branch}\n" - f"All steps on single branch: {self.state.work_branch}\n" - ) - - try: - run_git(ascend_path, "add", "-A") - staged = run_git(ascend_path, "diff", "--cached", "--name-only").strip() - if staged: - print_info(f"Files staged ({len(staged.splitlines())}):") - for f in staged.splitlines()[:10]: - print_info(f" - {f}") - if len(staged.splitlines()) > 10: - print_info(f" ... and {len(staged.splitlines()) - 10} more") - run_git(ascend_path, "commit", "-s", "-m", commit_msg) - print_status(True, f"Committed step {step_id}") - self.state.summary_rows.append( - (f"Commit {step_id}", "PASS", f"{step['commit_count']} commits") - ) - except subprocess.CalledProcessError as e: - stderr = (e.stderr or "").strip() if hasattr(e, 'stderr') else str(e) - if "nothing to commit" in stderr.lower(): - print_info(f"[{step_id}] Nothing to commit (clean merge)") - self.state.summary_rows.append( - (f"Commit {step_id}", "PASS", "No changes (clean merge)")) - else: - print_warn(f"Could not commit step {step_id}: {stderr[-200:]}") - self.state.summary_rows.append( - (f"Commit {step_id}", "WARN", stderr[:40])) - - # ═══════════════════════════════════════════════════════════════════════════ - # Phase 3+4: IR Compatibility Patch Loop - # ═══════════════════════════════════════════════════════════════════════════ - - def _llvm_hash_did_change(self) -> bool: - """Check if cmake/llvm-hash.txt differs from the Ascend baseline LLVM. - - The Ascend backend OP usage is based on a fixed baseline LLVM version. - If the target LLVM hash differs from the baseline, IR compatibility - patches need to be generated. - """ - ascend_path = Path(self.state.triton_ascend_path) - try: - current_hash = (ascend_path / "cmake" / "llvm-hash.txt") \ - .read_text(encoding="utf-8").strip() - except Exception: - return False - - old_hash = _ASCEND_BASELINE_LLVM_HASH - changed = old_hash != current_hash - if changed: - print_info(f"LLVM hash changed from baseline: " - f"{old_hash[:12]} → {current_hash[:12]}") - else: - print_info("LLVM hash matches baseline — skipping IR patch phase") - return changed - - def _do_ir_patch_loop(self) -> bool: - """Phase 3+4 outer loop: IR analysis → patch → rebuild → test → fix. - - Runs AFTER all progressive merge steps have completed. If - cmake/llvm-hash.txt didn't change, skips IR patches and goes - directly to pytest. - - Outer loop (max IR_MAX_ITERATIONS rounds): - [3.1-3.3] AI: analyze OPs, analyze changes, generate patches - [3.4-3.5] Apply patches to LLVM + rebuild - [4.1-4.2] Build TA + run pytest - [4.3] If failures, AI classifies (IR vs code) - → IR issues: loop back to modify patches - → Code issues: AI fix inner loop - Returns True if all tests pass, False on exhaustion. - """ - ascend_path = Path(self.state.triton_ascend_path) - - # ── Skip IR patch phase via env var ── - if os.getenv("SKIP_IR_PATCH", "false").lower() == "true": - print_header("Phase 3+4: IR Patch + Pytest — SKIPPED (SKIP_IR_PATCH=true)") - self.state.summary_rows.append( - ("IR Patch", "SKIP", "SKIP_IR_PATCH set")) - return True - - # ── Skip if LLVM hash unchanged ── - self.state.llvm_hash_changed = self._llvm_hash_did_change() - if not self.state.llvm_hash_changed: - print_header("Phase 4: Pytest (LLVM unchanged)") - print_info("LLVM hash unchanged — skipping IR analysis and patch generation") - return self._do_pytest() - - print_header("Phase 3: IR Compatibility Patch Auto-Generation") - print_info(f"LLVM hash changed — IR compatibility analysis required") - print_key_value("Baseline LLVM", _ASCEND_BASELINE_LLVM_HASH[:12]) - - self._print_workspace_info("Phase 3: IR Patch Loop") - print_key_value("Max IR iterations", str(self.state.ir_max_iterations)) - - for iteration in range(self.state.ir_max_iterations): - self.state.ir_patch_iteration = iteration - print_header( - f"IR Patch Loop — Iteration {iteration + 1}/" - f"{self.state.ir_max_iterations}" - ) - - # ── [3.1 + 3.2] Analysis (only on first iteration) ── - if iteration == 0: - print_info("First iteration — running full OP analysis pipeline") - if not self._do_ir_op_analysis(): - return False - if not self._do_ir_change_analysis(): - return False - else: - # On retry, re-analyze changes (patches from previous - # iteration may have altered the picture) - print_info("Re-analyzing OP changes after patch retry...") - if not self._do_ir_change_analysis(): - return False - - # ── [3.3] Generate patches ── - print_info("Step 3.3: Invoking AI to generate IR compatibility patches...") - if not self._do_ir_generate_patches(): - return False - - # ── [3.4 + 3.5] Apply patches + rebuild LLVM ── - print_info("Step 3.4-3.5: Applying patches and rebuilding LLVM (this may take a while)...") - if not self._do_ir_apply_patches_and_rebuild(): - return False - - # ── [4.1] Build TA ── - print_info("Step 4.1: Building Triton-Ascend with patched LLVM...") - if not self._do_build(ascend_path, clean=True): - if os.getenv("SKIP_AI_ANALYSIS", "false").lower() == "true": - return False - self.state.fix_errors = [str(WORKSPACE_DIR / BUILD_RESULT_FILE)] - self.state.build_fix_count += 1 - print_warn("Build failed after IR patches — will attempt AI fix") - self._do_ai_fix(ascend_path, WORKSPACE_DIR, 1) - continue - - # ── [4.2] Pytest ── - print_info("Step 4.2: Running pytest suite...") - if self._do_pytest(): - print_status(True, "All tests pass!") - self._commit_fixes(ascend_path, WORKSPACE_DIR) - self.state.ir_loop_details.append({ - "iteration": iteration + 1, - "result": "ALL_PASS", - }) - return True - - # ── [4.3] Diagnose failures ── - print_info("Step 4.3: Invoking AI to classify test failures (IR vs code)...") - has_ir_issues = self._do_ir_diagnose_failures() - if has_ir_issues: - self.state.ir_issues_found += 1 - print_warn( - f"IR compatibility issues found in iteration " - f"{iteration + 1} — retrying with modified patches" - ) - self.state.ir_loop_details.append({ - "iteration": iteration + 1, - "result": "IR_RETRY", - "ir_issues": self.state.ir_issues_found, - }) - continue - - # ── [4.4] Non-IR issues → AI fix inner loop ── - print_info("Non-IR failures detected — entering AI fix loop") - print_key_value("Max fix attempts", str(self.state.max_retries)) - for fix_attempt in range(1, self.state.max_retries + 1): - print_header(f"AI Fix Attempt {fix_attempt}/{self.state.max_retries}") - self.state.retry_count = fix_attempt - self._do_ai_fix(ascend_path, WORKSPACE_DIR, fix_attempt) - if not self._do_build(ascend_path, clean=False): - self.state.fix_errors = [str(WORKSPACE_DIR / BUILD_RESULT_FILE)] - continue - if self._do_pytest(): - self._commit_fixes(ascend_path, WORKSPACE_DIR) - self.state.ir_loop_details.append({ - "iteration": iteration + 1, - "result": "PASS_AFTER_FIX", - "fix_attempts": fix_attempt, - }) - return True - - print_error(f"All {self.state.max_retries} fix attempts exhausted " - f"in iteration {iteration + 1}") - self.state.ir_loop_details.append({ - "iteration": iteration + 1, - "result": "FIX_EXHAUSTED", - }) - - print_error(f"IR patch loop exhausted {self.state.ir_max_iterations} " - f"iterations") - return False - - def _do_ir_op_analysis(self) -> bool: - """[3.1] AI analyzes which MLIR OPs the Ascend backend uses.""" - print_header("Phase 3.1: IR OP Analysis") - ascend_path = Path(self.state.triton_ascend_path) - - self._print_workspace_info("Phase 3.1: IR OP Analysis") - - ir_dir = WORKSPACE_DIR / IR_ANALYSIS_DIR - ir_dir.mkdir(parents=True, exist_ok=True) - - from TA_main2main_workflow.agent.opencode_adapter import _detect_backend - backend = _detect_backend() - print_info(f"AI backend: {backend}") - print_key_value("Triton-Ascend", str(ascend_path)) - print_key_value("Output dir", str(ir_dir)) - - # ── Pre-scan: find candidate files with MLIR OP usage ── - print_info("Pre-scanning Ascend backend for MLIR OP patterns...") - candidate_files: list[str] = [] - scan_dirs = [ - ascend_path / "third_party" / "ascend" / "lib", - ascend_path / "lib" / "Target" / "Ascend", - ] - op_patterns = [ - r'::create\b', r'::get\b', r'\.match\b', r'\.walk\b', - r'isa<', r'cast<', r'dyn_cast<', - ] - for sd in scan_dirs: - if not sd.exists(): - print_warn(f"Scan dir not found: {sd}") - continue - for pattern in op_patterns: - try: - result = subprocess.run( - ["grep", "-rl", pattern, str(sd)], - capture_output=True, text=True, timeout=30, - ) - for f in result.stdout.splitlines(): - if f not in candidate_files: - candidate_files.append(f) - except (subprocess.TimeoutExpired, Exception): - pass - - candidate_files.sort() - print_info(f"Found {len(candidate_files)} candidate files with MLIR OP patterns") - for f in candidate_files[:15]: - print_info(f" - {Path(f).relative_to(ascend_path)}") - if len(candidate_files) > 15: - print_info(f" ... and {len(candidate_files) - 15} more files") - - # Write candidate file list for AI reference - hint_path = ir_dir / "candidate_files.txt" - hint_path.write_text("\n".join(candidate_files), encoding="utf-8") - print_info(f"Candidate file list written to {hint_path}") - - print_info("AI will scan candidate files for MLIR OP usage and output structured JSON") - print_info("Invoking AI for IR OP analysis (this may take several minutes)...") - - try: - ai_result = run_opencode_adapter({ - "step_id": "ir-analyze-ops", - "previous_step_id": "", - "previous_step_summary_path": "", - "is_last_step": "true", - "step_index": "ir", - "step_dir": str(ir_dir), - "fix_dir": str(ir_dir), - "conflict_dir": "", - "ascend_path": str(ascend_path), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "ir_analyze_ops", - "error_logs": "[]", - "target_commit": self.state.target_commit, - "llvm_project_path": str(_llvm_project_path()), - }) - _ = ai_result - except Exception as e: - print_error(f"IR OP analysis failed: {e}") - self.state.summary_rows.append(("IR OP Analysis", "FAIL", str(e)[:60])) - return False - - ops_report = ir_dir / IR_OPS_REPORT_FILE - if ops_report.exists(): - try: - data = json.loads(ops_report.read_text(encoding="utf-8")) - # ── Content validation: must have 'ops' array with real OP data ── - ops_list = data.get("ops", []) - if not ops_list or not isinstance(ops_list, list): - print_error( - f"AI output is NOT a valid OP report! " - f"Missing or empty 'ops' array. " - f"Top-level keys: {list(data.keys())}") - print_warn( - f"AI may have produced a merge analysis instead of IR OP scan. " - f"Check {ops_report} for content.") - self.state.summary_rows.append( - ("IR OP Analysis", "FAIL", - f"No 'ops' array — AI produced wrong output type")) - return False - # Check that ops have expected fields - valid_ops = [o for o in ops_list if isinstance(o, dict) and "name" in o] - if len(valid_ops) < len(ops_list): - print_warn( - f"{len(ops_list) - len(valid_ops)} entries missing 'name' field — filtered") - if not valid_ops: - print_error("No valid OP entries with 'name' field found!") - self.state.summary_rows.append( - ("IR OP Analysis", "FAIL", "No valid OP entries")) - return False - - self.state.ir_ops_report = data - dialects = data.get("dialects", []) - print_status(True, - f"OP analysis complete: " - f"{data.get('total_ops', len(valid_ops))} OPs, " - f"{len(dialects)} dialects — " - f"{', '.join(dialects[:10])}") - self.state.summary_rows.append( - ("IR OP Analysis", "PASS", - f"{data.get('total_ops', len(valid_ops))} OPs")) - return True - except Exception as e: - print_warn(f"Could not parse ops report: {e}") - - self.state.summary_rows.append(("IR OP Analysis", "FAIL", "No report")) - return False - - def _do_ir_change_analysis(self) -> bool: - """[3.2] AI analyzes OP definition changes between LLVM versions.""" - print_header("Phase 3.2: IR OP Change Analysis") - ascend_path = Path(self.state.triton_ascend_path) - - self._print_workspace_info("Phase 3.2: IR Change Analysis") - - ir_dir = WORKSPACE_DIR / IR_ANALYSIS_DIR - ir_dir.mkdir(parents=True, exist_ok=True) - - baseline_hash = _ASCEND_BASELINE_LLVM_HASH - llvm_project = _llvm_project_path() - - # Read target LLVM hash from ascend repo - llvm_hash_file = ascend_path / "cmake" / "llvm-hash.txt" - if not llvm_hash_file.exists(): - print_error(f"llvm-hash.txt not found at {llvm_hash_file}") - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", "llvm-hash.txt missing")) - return False - target_hash = llvm_hash_file.read_text(encoding="utf-8").strip() - - print_key_value("Input ops report", str(ir_dir / IR_OPS_REPORT_FILE)) - print_key_value("LLVM project", str(llvm_project)) - print_key_value("Baseline LLVM", f"{baseline_hash[:12]} ({baseline_hash})") - print_key_value("Target LLVM", f"{target_hash[:12]} ({target_hash})") - - # ── Pre-flight: verify both commits exist in llvm-project ── - if not llvm_project.exists(): - print_error(f"llvm-project not found at {llvm_project}") - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", "llvm-project not found")) - return False - - print_info("Verifying LLVM commits are available in llvm-project...") - for label, h in [("Baseline", baseline_hash), ("Target", target_hash)]: - try: - result = subprocess.run( - ["git", "cat-file", "-t", h], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=30, - ) - if result.returncode == 0: - print_status(True, f"{label} commit {h[:12]} — found in llvm-project") - else: - print_error( - f"{label} commit {h[:12]} NOT found in llvm-project! " - f"(git cat-file -t returned: {result.stderr.strip()})") - print_warn( - f"Try: cd {llvm_project} && git fetch origin {h}") - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", - f"{label} commit {h[:12]} not in llvm-project")) - return False - except subprocess.TimeoutExpired: - print_error(f"Timeout checking {label} commit {h[:12]}") - return False - except Exception as e: - print_error(f"Failed to verify {label} commit: {e}") - return False - - # ── Pre-flight: show MLIR .td file changes between the two commits ── - print_info("Scanning MLIR .td file changes between baseline and target...") - try: - diff_result = subprocess.run( - ["git", "diff", "--name-only", baseline_hash, target_hash, - "--", "mlir/include/"], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=60, - ) - if diff_result.returncode == 0: - changed_files = [f for f in diff_result.stdout.splitlines() - if f.endswith(".td")] - print_info(f"Found {len(changed_files)} changed .td files in mlir/include/ " - f"between baseline and target") - for f in changed_files[:20]: - print_info(f" - {f}") - if len(changed_files) > 20: - print_info(f" ... and {len(changed_files) - 20} more .td files") - else: - print_warn(f"git diff returned non-zero: {diff_result.stderr.strip()}") - except subprocess.TimeoutExpired: - print_warn("git diff timed out after 60s — continuing anyway") - except Exception as e: - print_warn(f"Could not run git diff for .td files: {e}") - - # ── Pre-flight: show ops report summary for AI context ── - ops_report_path = ir_dir / IR_OPS_REPORT_FILE - if ops_report_path.exists(): - try: - ops = json.loads(ops_report_path.read_text(encoding="utf-8")) - print_info( - f"Ops report: {ops.get('total_ops', '?')} OPs across " - f"{len(ops.get('dialects', []))} dialects — " - f"{', '.join(ops.get('dialects', [])[:8])}") - except Exception: - print_warn("Could not read ops_report.json for summary") - else: - print_warn(f"Ops report not found at {ops_report_path} — " - f"AI will need to discover OPs on its own") - - print_info("AI will compare each OP's .td definition with:") - print_info(f" git show {baseline_hash[:12]}:mlir/include/.../.td") - print_info(f" git show {target_hash[:12]}:mlir/include/.../.td") - print_info("Invoking AI for OP change analysis (this may take several minutes)...") - - try: - ai_result = run_opencode_adapter({ - "step_id": "ir-analyze-changes", - "previous_step_id": "ir-analyze-ops", - "previous_step_summary_path": str(ir_dir / IR_OPS_REPORT_FILE), - "is_last_step": "true", - "step_index": "ir", - "step_dir": str(ir_dir), - "fix_dir": str(ir_dir), - "conflict_dir": "", - "ascend_path": str(ascend_path), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "ir_analyze_changes", - "error_logs": json.dumps( - [str(ir_dir / IR_OPS_REPORT_FILE)], ensure_ascii=False), - "target_commit": self.state.target_commit, - "llvm_project_path": str(_llvm_project_path()), - "baseline_llvm_hash": _ASCEND_BASELINE_LLVM_HASH, - "target_llvm_hash": target_hash, - }) - _ = ai_result - except Exception as e: - print_error(f"IR change analysis failed: {e}") - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", str(e)[:60])) - return False - - changes_report = ir_dir / IR_CHANGES_REPORT_FILE - if changes_report.exists(): - try: - data = json.loads(changes_report.read_text(encoding="utf-8")) - # ── Content validation: must have 'changes' array and 'summary' ── - changes_list = data.get("changes", []) - summary = data.get("summary", {}) - if not changes_list or not isinstance(changes_list, list): - print_error( - f"AI output is NOT a valid changes report! " - f"Missing or empty 'changes' array. " - f"Top-level keys: {list(data.keys())}") - print_warn( - f"AI may have produced a merge analysis instead of " - f"OP change comparison. Check {changes_report} for content.") - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", - "No 'changes' array — AI produced wrong output type")) - return False - - self.state.ir_changes_report = data - print_status(True, - f"Change analysis: {summary.get('total_ops_analyzed', '?')} " - f"OPs, {summary.get('ops_needing_patch', '?')} need patch, " - f"{summary.get('renamed_ops', 0)} renamed, " - f"{summary.get('signature_changes', 0)} signature changes") - self.state.summary_rows.append( - ("IR Change Analysis", "PASS", - f"{summary.get('ops_needing_patch', '?')} OPs need patch")) - # If no OPs need patching, still return True (Phase 3 is a no-op) - return True - except Exception as e: - print_warn(f"Could not parse changes report: {e}") - - self.state.summary_rows.append( - ("IR Change Analysis", "FAIL", "No report")) - return False - - def _do_ir_generate_patches(self) -> bool: - """[3.3] AI modifies the Ascend LLVM patch for IR compatibility. - - The AI directly edits the existing patch file at - ``third_party/ascend/patch/llvm_patch_f6ded0b.patch`` rather than - creating a new file from scratch — this lets it start from a known- - working baseline and only adjust the parts that need changing for - the current LLVM version. - """ - print_header("Phase 3.3: IR Patch Generation") - ascend_path = Path(self.state.triton_ascend_path) - - self._print_workspace_info("Phase 3.3: IR Patch Generation") - - ir_dir = WORKSPACE_DIR / IR_ANALYSIS_DIR - - # The patch file that AI modifies in-place - ascend_patch = (ascend_path / "third_party" / "ascend" / "patch" - / "llvm_patch_f6ded0b.patch") - print_key_value("Target patch", str(ascend_patch)) - - changes_report = ir_dir / IR_CHANGES_REPORT_FILE - if changes_report.exists(): - try: - report = json.loads(changes_report.read_text(encoding="utf-8")) - summary = report.get("summary", {}) - print_info(f"Changes report: {summary.get('total_ops_analyzed', '?')} OPs analyzed, " - f"{summary.get('ops_needing_patch', '?')} need patches") - except Exception: - pass - print_info("Invoking AI to modify the Ascend LLVM compatibility patch...") - - try: - ai_result = run_opencode_adapter({ - "step_id": "ir-generate-patch", - "previous_step_id": "ir-analyze-changes", - "previous_step_summary_path": str(ir_dir / IR_CHANGES_REPORT_FILE), - "is_last_step": "true", - "step_index": "ir", - "step_dir": str(ascend_patch.parent), - "fix_dir": str(ascend_patch.parent), - "conflict_dir": "", - "ascend_path": str(ascend_path), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "ir_generate_patch", - "error_logs": json.dumps( - [str(ir_dir / IR_CHANGES_REPORT_FILE)], ensure_ascii=False), - "target_commit": self.state.target_commit, - "llvm_project_path": str(_llvm_project_path()), - "baseline_llvm_hash": _ASCEND_BASELINE_LLVM_HASH, - "ascend_patch_file": str(ascend_patch), - }) - _ = ai_result - except Exception as e: - print_error(f"IR patch generation failed: {e}") - self.state.summary_rows.append( - ("IR Patch Gen", "FAIL", str(e)[:60])) - return False - - # Check the ascend patch was modified - if ascend_patch.exists(): - print_status(True, f"Modified {ascend_patch.name}") - self.state.ir_patches = [str(ascend_patch)] - self.state.summary_rows.append( - ("IR Patch Gen", "PASS", ascend_patch.name)) - return True - - # No changes needed — valid if changes_report showed no issues - print_info(f"{ascend_patch.name} unchanged — " - "IR compatibility may already be satisfied") - self.state.summary_rows.append( - ("IR Patch Gen", "PASS", "No changes needed")) - return True - - def _do_ir_apply_patches_and_rebuild(self) -> bool: - """[3.4 + 3.5] Apply the Ascend LLVM patch and rebuild.""" - print_header("Phase 3.4-3.5: Apply Patches + Rebuild LLVM") - ascend_path = Path(self.state.triton_ascend_path) - - self._print_workspace_info("Phase 3.4-3.5: Apply Patches + Rebuild LLVM") - - llvm_project = _llvm_project_path() - # The in-repo Ascend LLVM patch (modified by AI in step 3.3) - ascend_patch = (ascend_path / "third_party" / "ascend" / "patch" - / "llvm_patch_f6ded0b.patch") - print_key_value("LLVM project", str(llvm_project)) - print_key_value("Patch file", str(ascend_patch)) - - if not llvm_project.exists(): - print_error(f"LLVM project not found at {llvm_project}") - self.state.summary_rows.append( - ("IR Apply+Rebuild", "FAIL", "llvm-project not found")) - return False - - # ── Ensure llvm-project workspace is clean before checkout + patch ── - if not self._ensure_llvm_workspace_clean(reason="ir-apply-patches"): - print_error("Cannot clean llvm-project workspace — aborting IR patch rebuild") - self.state.summary_rows.append( - ("IR Apply+Rebuild", "FAIL", "workspace not clean")) - return False - - # ── [3.4] Apply patch to llvm-project ── - # Deterministic: clean → checkout → apply. No AI involved. - from TA_main2main_workflow.scripts.build_test import apply_llvm_patches - - # Read the target LLVM hash from triton-ascend - llvm_hash_file = ascend_path / "cmake" / "llvm-hash.txt" - target_llvm_hash = "" - if llvm_hash_file.exists(): - target_llvm_hash = llvm_hash_file.read_text(encoding="utf-8").strip() - print_key_value("Target LLVM hash", target_llvm_hash[:12]) - - print_info(f"Step 3.4: Applying {ascend_patch.name} to llvm-project...") - patch_result = apply_llvm_patches( - ascend_patch.parent, llvm_project, - target_hash=target_llvm_hash, patch_file=ascend_patch) - if not patch_result["all_ok"]: - failed = patch_result["failed"] - print_error(f"LLVM patch apply failed: " - f"{failed[0]['error'][:200] if failed else 'unknown'}") - self.state.summary_rows.append( - ("IR Apply+Rebuild", "FAIL", "patch did not apply cleanly")) - return False - - print_status(True, f"{ascend_patch.name} applied to llvm-project") - - # ── [3.5] Rebuild LLVM ── - from TA_main2main_workflow.scripts.build_test import \ - _check_and_rebuild_llvm - try: - print_info("Step 3.5: Rebuilding LLVM (this takes ~1-2 hours)...") - llvm_prefix = _check_and_rebuild_llvm( - ascend_path, force_rebuild=True) - if llvm_prefix and not self.state.llvm_prefix: - self.state.llvm_prefix = llvm_prefix - print_status(True, "LLVM rebuild complete") - self.state.summary_rows.append( - ("LLVM Patch Apply+Rebuild", "PASS", "patch applied, LLVM rebuilt")) - return True - except Exception as e: - print_error(f"LLVM rebuild failed: {e}") - self.state.summary_rows.append( - ("LLVM Patch Apply+Rebuild", "FAIL", str(e)[:60])) - return False - - def _do_pytest(self) -> bool: - """[4.2] Build TA and run pytest. - - Returns True when all tests pass. - """ - ascend_path = Path(self.state.triton_ascend_path) - py_exe = os.getenv("PYTHON", "python3.10") - - import shutil - if not shutil.which(py_exe): - print_warn(f"{py_exe} not found on PATH — skipping tests") - self.state.pytest_passed = False - self.state.summary_rows.append( - ("Pytest", "SKIP", f"{py_exe} not found")) - return False - - print_section(f"Pytest ({py_exe})") - print_key_value("Ascend path", str(ascend_path)) - - # Build with test python - print_info(f"Building Triton-Ascend with {py_exe}...") - if not self._do_build(ascend_path, clean=True, python_exe=py_exe): - print_error(f"Build failed ({py_exe})") - self.state.pytest_passed = False - self.state.summary_rows.append( - ("Pytest", "FAIL", "Build failed")) - return False - - # Run tests - result = self._do_test(ascend_path, python_exe=py_exe) - if result is None: - passed = True # SKIP_E2E_TEST - else: - passed = bool(result) - - self.state.pytest_passed = passed - if not passed: - print_error(f"Pytest FAILED ({py_exe})") - - self.state.summary_rows.append( - ("Pytest", "PASS" if passed else "FAIL", py_exe)) - return passed - - def _do_ir_diagnose_failures(self) -> bool: - """[4.3] AI classifies test failures: IR compatibility vs code issues. - - Returns True if IR issues are found (triggering outer loop retry). - Returns False if failures are all code/environment issues. - """ - print_header("Phase 4.3: IR Failure Diagnosis") - - ir_dir = WORKSPACE_DIR / IR_ANALYSIS_DIR - ir_dir.mkdir(parents=True, exist_ok=True) - print_key_value("Diagnosis output", str(ir_dir / IR_DIAGNOSIS_FILE)) - - # Collect test failure logs from both Python runs - error_log_paths: list[str] = [] - test_log_dir = WORKSPACE_DIR / "test-logs" - if test_log_dir.exists(): - for log_file in sorted(test_log_dir.rglob("*.log")): - error_log_paths.append(str(log_file)) - # Also include test result files - test_result = WORKSPACE_DIR / TEST_RESULT_FILE - if test_result.exists(): - error_log_paths.append(str(test_result)) - - if not error_log_paths: - print_warn("No test failure logs found — assuming code issues") - return False - - print_info(f"Collected {len(error_log_paths)} log file(s) for AI diagnosis") - for p in error_log_paths[:5]: - print_info(f" - {p}") - if len(error_log_paths) > 5: - print_info(f" ... and {len(error_log_paths) - 5} more") - print_info("Invoking AI to classify failures (IR compatibility vs code vs environment)...") - - try: - ai_result = run_opencode_adapter({ - "step_id": "ir-diagnose", - "previous_step_id": "ir-generate-patch", - "previous_step_summary_path": str(ir_dir / IR_CHANGES_REPORT_FILE), - "is_last_step": "true", - "step_index": "ir", - "step_dir": str(ir_dir), - "fix_dir": str(ir_dir), - "conflict_dir": "", - "ascend_path": str(Path(self.state.triton_ascend_path)), - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "ir_diagnose", - "error_logs": json.dumps(error_log_paths, ensure_ascii=False), - "target_commit": self.state.target_commit, - }) - _ = ai_result - except Exception as e: - print_error(f"IR diagnosis failed: {e}") - return False - - diagnosis_path = ir_dir / IR_DIAGNOSIS_FILE - if not diagnosis_path.exists(): - print_warn("No diagnosis report generated") - return False - - try: - diagnosis = json.loads( - diagnosis_path.read_text(encoding="utf-8")) - summary = diagnosis.get("summary", {}) - has_ir = summary.get("has_ir_issues", False) - print_key_value("total failures", str(summary.get("total_failures", "?"))) - print_key_value("IR issues", str(summary.get("ir_issues", "?"))) - print_key_value("code issues", str(summary.get("code_issues", "?"))) - print_key_value("env issues", str(summary.get("environment_issues", "?"))) - self.state.summary_rows.append( - ("IR Diagnosis", "PASS", - f"IR={summary.get('ir_issues', '?')} " - f"code={summary.get('code_issues', '?')} " - f"env={summary.get('environment_issues', '?')}")) - return bool(has_ir) - except Exception as e: - print_warn(f"Could not parse diagnosis: {e}") - return False - - # ═══════════════════════════════════════════════════════════════════════════ - # Per-step IR patch pipeline (single-step mode) - # ═══════════════════════════════════════════════════════════════════════════ - - def _do_per_step_ir_patch(self, step: dict) -> bool: - """Per-step IR compatibility patch generation + LLVM rebuild. - - Called from _run_single_step_mode() when a step's merge included an - LLVM hash change. Reuses the shared IR analysis / patch-generation - methods but adds a patch→rebuild retry loop specific to the - single-step context. - - Pipeline: - 1. Verify LLVM hash changed - 2. Create llvm_change_analysis// workspace - 3. IR analysis → patch → apply → rebuild LLVM (max 3 iterations) - 4. On patch failure: stash/drop, loop back for AI to fix - """ - step_id = step["id"] - - # ── Guard: check LLVM hash actually changed ── - if not self._llvm_hash_did_change(): - print_info(f"[{step_id}] LLVM hash unchanged — skipping IR patch") - return True - - # ── Create per-step analysis workspace ── - analysis_dir = WORKSPACE_DIR / LLVM_CHANGE_ANALYSIS_DIR / step_id - analysis_dir.mkdir(parents=True, exist_ok=True) - print_key_value("IR analysis dir", str(analysis_dir)) - - # ── IR analysis → patch → rebuild loop ── - for iteration in range(self.state.ir_max_iterations): - self.state.ir_patch_iteration = iteration - print_header( - f"Per-Step IR Patch — {step_id} " - f"(iter {iteration + 1}/{self.state.ir_max_iterations})" - ) - - # [3.1 + 3.2] Analysis (first iteration only for OP scan) - if iteration == 0: - print_info("First iteration — running full OP analysis pipeline") - if not self._do_ir_op_analysis(): - return False - if not self._do_ir_change_analysis(): - return False - else: - print_info("Re-analyzing OP changes after patch retry...") - if not self._do_ir_change_analysis(): - return False - - # [3.3] Generate patches - if not self._do_ir_generate_patches(): - return False - - # [3.4 + 3.5] Apply patches + rebuild LLVM (with retry for patch failures) - rebuild_ok = False - for patch_attempt in range(IR_MAX_ITERATIONS): - print_info( - f"Patch apply attempt {patch_attempt + 1}/{IR_MAX_ITERATIONS}" - ) - if self._do_ir_apply_patches_and_rebuild(): - rebuild_ok = True - break - # Patch failed — stash/drop, let AI regenerate - print_warn( - f"LLVM rebuild failed (patch attempt {patch_attempt + 1}) — " - f"will stash changes and retry patch generation" - ) - self._stash_and_drop_llvm_patch() - if not self._do_ir_generate_patches(): - break - - if rebuild_ok: - print_status(True, f"IR patch + LLVM rebuild OK for {step_id}") - self.state.ir_loop_details.append({ - "step_id": step_id, - "iteration": iteration + 1, - "result": "PASS", - }) - return True - - print_warn(f"IR patch iteration {iteration + 1} exhausted " - f"— retrying outer loop") - - print_error(f"IR patch loop exhausted {self.state.ir_max_iterations} " - f"iterations for {step_id}") - return False - - def _build_baseline_llvm(self) -> bool: - """Build baseline LLVM (pre-merge state) before any merge steps. - - Called once at the start of _run_single_step_mode(). Reads the - current cmake/llvm-hash.txt from triton-ascend, checks out that - commit in llvm-project, applies the Ascend backend LLVM patch, - builds LLVM, then stashes + drops the patch to leave a clean tree. - - The baseline LLVM must be built before merging because the Ascend - backend code depends on it for compilation. - """ - print_header("Build Baseline LLVM (pre-merge)") - ascend_path = Path(self.state.triton_ascend_path) - - self._print_workspace_info("Build Baseline LLVM") - - # ── Allow skipping baseline LLVM build for debugging ── - if os.getenv("SKIP_BASELINE_LLVM", "false").lower() == "true": - print_info("SKIP_BASELINE_LLVM=true — skipping baseline LLVM build") - print_warn("Ensure LLVM is already built at LLVM_INSTALL_PREFIX_SYNC") - if not self.state.llvm_prefix: - self.state.llvm_prefix = str(_llvm_install_prefix()) - self.state.summary_rows.append( - ("Baseline LLVM", "SKIP", "SKIP_BASELINE_LLVM set")) - return True - - llvm_project = _llvm_project_path() - llvm_install = _llvm_install_prefix() - - if not llvm_project.exists(): - print_error(f"llvm-project not found at {llvm_project}") - return False - - # ── 1. Read LLVM hash from base branch (work branch base) ── - # Use git show to get the hash from the base branch, NOT the checkout - # filesystem — the checkout may be on a stale branch. - base_branch = os.getenv(ENV_BASE_BRANCH, "main") - base_ref = get_base_branch_ref() - try: - run_git(ascend_path, "fetch", "origin", base_branch) - except Exception: - print_warn(f"[baseline-llvm] Could not fetch {base_ref}, using local ref") - try: - llvm_hash = run_git( - ascend_path, "show", f"{base_ref}:cmake/llvm-hash.txt" - ).strip() - except Exception: - # Fallback: read from checkout filesystem - llvm_hash_file = ascend_path / "cmake" / "llvm-hash.txt" - if not llvm_hash_file.exists(): - print_error(f"LLVM hash file not found: {llvm_hash_file}") - return False - llvm_hash = llvm_hash_file.read_text(encoding="utf-8").strip() - print_warn(f"[baseline-llvm] Using checkout llvm-hash.txt ({base_ref} not available)") - if not llvm_hash: - print_error("LLVM hash is empty") - return False - print_key_value("LLVM commit", llvm_hash[:12]) - print_info(f" (from {base_ref})") - - # ── Ensure llvm-project workspace is clean before checkout ── - if not self._ensure_llvm_workspace_clean(reason="baseline-llvm-build"): - print_error("Cannot clean llvm-project workspace — aborting baseline build") - return False - - # ── 2. Checkout the LLVM commit ── - print_info(f"Checking out LLVM commit {llvm_hash[:12]} in llvm-project...") - try: - # Fetch the specific commit with retries - for attempt in range(1, 7): - fetch_proc = subprocess.run( - ["git", "fetch", "origin", llvm_hash], - cwd=str(llvm_project), capture_output=True, text=True, timeout=2000, - ) - if fetch_proc.returncode == 0: - break - print_warn(f"git fetch attempt {attempt}/6 failed: " - f"{fetch_proc.stderr.strip()[-150:]}") - else: - raise RuntimeError( - f"Failed to fetch LLVM commit {llvm_hash[:12]} after 6 attempts") - - subprocess.run( - ["git", "checkout", llvm_hash], - cwd=str(llvm_project), check=True, capture_output=True, text=True, - timeout=2000, - ) - print_status(True, f"Checked out {llvm_hash[:12]}") - except Exception as e: - print_error(f"Failed to checkout LLVM commit: {e}") - log_proc = subprocess.run( - ["git", "log", "--oneline", "-5"], - cwd=str(llvm_project), capture_output=True, text=True, timeout=10, - ) - print_info(f"llvm-project HEAD and recent commits:\n{log_proc.stdout.strip()}") - return False - - # ── 3. Apply Ascend backend LLVM patch ── - ascend_patch = ascend_path / "third_party" / "ascend" / "patch" / "llvm_patch_f6ded0b.patch" - if ascend_patch.exists(): - print_info(f"Applying Ascend LLVM patch: {ascend_patch.name}") - # Dry-run first - dry_run = subprocess.run( - ["git", "apply", "--check", str(ascend_patch)], - cwd=str(llvm_project), capture_output=True, text=True, timeout=30, - ) - if dry_run.returncode != 0: - print_error(f"Patch does not apply cleanly: {dry_run.stderr.strip()[-400:]}") - return False - try: - subprocess.run( - ["git", "apply", str(ascend_patch)], - cwd=str(llvm_project), check=True, capture_output=True, text=True, timeout=30, - ) - print_status(True, "Ascend LLVM patch applied") - except Exception as e: - print_error(f"Failed to apply patch: {e}") - return False - else: - print_warn(f"Ascend LLVM patch not found at {ascend_patch} — continuing without it") - - # ── 4. Build LLVM ── - llvm_build_log = WORKSPACE_DIR / "llvm_build_baseline.log" - llvm_build_log.parent.mkdir(parents=True, exist_ok=True) - - build_dir = llvm_project / "build" - if build_dir.exists(): - import shutil - shutil.rmtree(build_dir) - build_dir.mkdir() - - cmake_cmd = [ - "cmake", str(llvm_project / "llvm"), - "-G", "Ninja", - "-DCMAKE_BUILD_TYPE=Release", - "-DLLVM_ENABLE_ASSERTIONS=ON", - "-DLLVM_ENABLE_PROJECTS=mlir;llvm;lld", - "-DLLVM_TARGETS_TO_BUILD=host;NVPTX;AMDGPU", - f"-DCMAKE_INSTALL_PREFIX={llvm_install}", - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_CXX_COMPILER=clang++", - ] - - # ── Helper: run a command with live output streaming ── - def _stream_cmd(cmd: list[str], cwd: Path, log_fh, timeout: int, - label: str) -> int: - """Stream subprocess output line-by-line to console and log file. - Returns the process exit code.""" - print_info(f"{label} (streaming to {llvm_build_log.name})...") - proc = subprocess.Popen( - cmd, cwd=str(cwd), - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - ) - assert proc.stdout is not None - last_line = "" - for line in proc.stdout: - log_fh.write(line) - stripped = line.rstrip() - if stripped: - last_line = stripped - # \r returns to line start, \033[K clears trailing residue - print(f"\r {stripped[:140]}\033[K", end="", flush=True) - proc.wait(timeout=timeout) - if last_line: - print() # final newline after \r lines - return proc.returncode - - # ── cmake configure ── - with llvm_build_log.open("w", encoding="utf-8") as fh: - fh.write(f"=== cmake ===\n{' '.join(cmake_cmd)}\n\n") - fh.flush() - rc = _stream_cmd(cmake_cmd, build_dir, fh, timeout=300, - label="Configuring LLVM with cmake") - if rc != 0: - print_error(f"cmake failed (exit {rc}) — see {llvm_build_log}") - return False - print_status(True, "cmake configure OK") - - # ── ninja build + install ── - print_info("Building LLVM with ninja (this may take ~0.5 hours)...") - with llvm_build_log.open("a", encoding="utf-8") as fh: - fh.write(f"\n=== ninja install ===\n") - fh.flush() - rc = _stream_cmd(["ninja", "install"], build_dir, fh, timeout=7200, - label="ninja install") - if rc != 0: - print_error(f"ninja install failed (exit {rc}) — see {llvm_build_log}") - return False - print_status(True, "ninja install OK") - - # Copy FileCheck - import shutil - filecheck_src = build_dir / "bin" / "FileCheck" - filecheck_dst = llvm_install / "bin" / "FileCheck" - if filecheck_src.exists(): - filecheck_dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(filecheck_src, filecheck_dst) - print_info("Copied FileCheck to install prefix") - - # Write hash cache - hash_cache = llvm_install / ".llvm_hash" - llvm_install.mkdir(parents=True, exist_ok=True) - hash_cache.write_text(llvm_hash, encoding="utf-8") - print_status(True, "Baseline LLVM build complete") - - # Store llvm_prefix for later use - if not self.state.llvm_prefix: - self.state.llvm_prefix = str(llvm_install) - - # ── 5. Stash + drop the patch to leave a clean tree ── - print_info("Stashing and dropping Ascend LLVM patch to clean working tree...") - try: - subprocess.run( - ["git", "stash", "push", "-u", "-m", "ta-baseline-llvm-patch"], - cwd=str(llvm_project), capture_output=True, text=True, timeout=30, - ) - subprocess.run( - ["git", "stash", "drop", "stash@{0}"], - cwd=str(llvm_project), capture_output=True, text=True, timeout=30, - ) - print_status(True, "LLVM working tree clean (patch stashed + dropped)") - except Exception as e: - print_warn(f"Stash/drop failed: {e} — forcing clean with checkout") - subprocess.run( - ["git", "checkout", "--", "."], - cwd=str(llvm_project), capture_output=True, text=True, timeout=60, - ) - subprocess.run( - ["git", "clean", "-fd"], - cwd=str(llvm_project), capture_output=True, text=True, timeout=60, - ) - - self.state.summary_rows.append( - ("Baseline LLVM", "PASS", f"Built {llvm_hash[:12]}")) - return True - - def _ensure_llvm_workspace_clean(self, reason: str = "") -> bool: - """Ensure the llvm-project working tree is clean before building. - - Checks git status; if dirty, stashes and drops all uncommitted - changes (including untracked files). Falls back to 'git checkout - -- .' + 'git clean -fd' if stash fails. - - Returns True if the workspace is clean (or was cleaned successfully). - """ - llvm_project = _llvm_project_path() - if not llvm_project.exists(): - print_warn("[llvm-clean] llvm-project not found — cannot verify workspace") - return True # nothing to clean - - # ── Check if working tree is dirty ── - try: - status = subprocess.run( - ["git", "status", "--porcelain"], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=15, - ).stdout.strip() - except Exception as e: - print_warn(f"[llvm-clean] Could not check git status: {e}") - return True # proceed and let the build step surface errors - - if not status: - print_info(f"[llvm-clean] llvm-project workspace is clean" - f"{f' ({reason})' if reason else ''}") - return True - - # ── Workspace is dirty — clean it ── - dirty_files = status.splitlines() - print_warn(f"[llvm-clean] llvm-project has {len(dirty_files)} uncommitted" - f" file(s){f' ({reason})' if reason else ''} — cleaning...") - for f in dirty_files[:10]: - print(f" {f}") - if len(dirty_files) > 10: - print(f" ... and {len(dirty_files) - 10} more") - - try: - subprocess.run( - ["git", "stash", "push", "-u", "-m", - f"ta-auto-clean{': ' + reason if reason else ''}"], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=30, - ) - subprocess.run( - ["git", "stash", "drop", "stash@{0}"], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=30, - ) - print_status(True, "[llvm-clean] Workspace cleaned (stash + drop)") - return True - except Exception as e: - print_warn(f"[llvm-clean] Stash/drop failed: {e} — " - f"forcing clean with checkout") - try: - subprocess.run( - ["git", "checkout", "--", "."], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=60, - ) - subprocess.run( - ["git", "clean", "-fd"], - cwd=str(llvm_project), - capture_output=True, text=True, timeout=60, - ) - print_status(True, "[llvm-clean] Workspace cleaned (checkout + clean)") - return True - except Exception as e2: - print_error(f"[llvm-clean] Failed to clean workspace: {e2}") - return False - - def _stash_and_drop_llvm_patch(self) -> None: - """Deprecated: use _ensure_llvm_workspace_clean() instead.""" - self._ensure_llvm_workspace_clean(reason="ir-patch-failed") - - # ═══════════════════════════════════════════════════════════════════════════ - # Per-step test + fix loop (single-step mode) - # ═══════════════════════════════════════════════════════════════════════════ - - def _do_test_and_fix_loop(self) -> bool: - """Run tests + AI-fix loop for the current step. - - Returns True if all tests pass, False on exhaustion. - """ - ascend_path = Path(self.state.triton_ascend_path) - step = (self.state.steps[self.state.current_step] - if self.state.steps else None) - step_id = step["id"] if step else "step-0" - step_dir = WORKSPACE_DIR / STEPS_DIR / step_id - step_dir.mkdir(parents=True, exist_ok=True) - - test_passed = False - - for attempt in range(self.state.max_retries + 1): - is_fix_attempt = attempt > 0 - self.state.retry_count = attempt - - # AI fix test failures (skip on first round) - if is_fix_attempt: - print_header(f"Fix Attempt {attempt}/{self.state.max_retries} (test)") - self.state.fix_errors = self._collect_test_error_logs() - if self.state.fix_errors: - ai_ok = self._do_ai_fix(ascend_path, step_dir, attempt) - # Record fix attempt - modified_files: list[str] = [] - ai_summary = "" - if hasattr(self, '_last_ai_result') and self._last_ai_result: - modified_files = self._last_ai_result.get("modified_files", []) - ai_summary = self._last_ai_result.get("step_summary", "") - error_snippet = "" - for err_path in self.state.fix_errors: - try: - content = Path(err_path).read_text( - encoding="utf-8", errors="replace") - error_snippet += (content[-2000:] - if len(content) > 2000 else content) - except Exception: - pass - self.state.fix_attempts.append({ - "step_id": step_id, - "attempt": attempt, - "fix_type": "test", - "error_logs": list(self.state.fix_errors), - "error_snippet": error_snippet[-1500:], - "modified_files": modified_files, - "ai_summary": (ai_summary or "")[:2000], - "ai_ok": ai_ok, - }) - self.state.test_fix_count += 1 - else: - print_warn("No test error logs found — cannot fix") - - # Rebuild after fix (skip on first attempt since build_and_fix already built) - if is_fix_attempt: - if not self._do_build(ascend_path, clean=False): - print_warn(f"Build failed after test fix (attempt {attempt})") - continue - - # Run tests - test_result = self._do_test(ascend_path) - if test_result is None: - # SKIP_E2E_TEST — treat as pass - test_passed = True - break - if test_result: - test_passed = True - break - - print_warn(f"Tests failed (attempt {attempt + 1}/" - f"{self.state.max_retries + 1})") - - if os.getenv("SKIP_AI_ANALYSIS", "false").lower() == "true": - print_warn("SKIP_AI_ANALYSIS=true — stopping test fix loop") - break - - if test_passed: - self.state.test_passed = True - # Commit test fixes if any were applied - if self.state.retry_count > 0: - self._commit_fixes(ascend_path, step_dir) - self.state.summary_rows.append( - ("Tests", "PASS", f"{step_id}")) - else: - print_error(f"All {self.state.max_retries} fix attempts exhausted " - f"— tests still failing") - self.state.summary_rows.append( - ("Tests", "FAIL", f"After {self.state.max_retries} attempts")) - self.state.test_passed = False - - return test_passed - - def _collect_test_error_logs(self) -> list[str]: - """Collect test failure log paths for AI fix context. - - Returns a list of file paths pointing to test logs and test result - files in the workspace. - """ - error_logs: list[str] = [] - - # Test log directory — includes raw logs and JUnit XML reports - test_log_dir = WORKSPACE_DIR / "test-logs" - if test_log_dir.exists(): - for log_file in sorted(test_log_dir.rglob("*.log")): - error_logs.append(str(log_file)) - for xml_file in sorted(test_log_dir.rglob("*.xml")): - error_logs.append(str(xml_file)) - - # Test result JSON - test_result_path = WORKSPACE_DIR / TEST_RESULT_FILE - if test_result_path.exists(): - error_logs.append(str(test_result_path)) - - # Build result JSON (may contain build errors that affect tests) - build_result_path = WORKSPACE_DIR / BUILD_RESULT_FILE - if build_result_path.exists(): - error_logs.append(str(build_result_path)) - - if error_logs: - print_info(f"Collected {len(error_logs)} error log(s) for AI fix") - for p in error_logs[:5]: - print_info(f" - {p}") - if len(error_logs) > 5: - print_info(f" ... and {len(error_logs) - 5} more") - - return error_logs - - def _backup_code_state(self, label: str = "snapshot") -> Path | None: - """Backup triton-ascend working tree to workspace for CI artifact retention. - - Copies the entire working tree (tracked + untracked) excluding .git - and build artifacts. Used both on success (label="final") and on - failure (label="failed-step-N") so no AI fix or conflict resolution - work is ever lost. - """ - ascend_path = Path(self.state.triton_ascend_path) - ts = time.strftime("%Y%m%d-%H%M%S") - backup_dir = WORKSPACE_DIR / "code-backups" / f"{label}_{ts}" - backup_dir.parent.mkdir(parents=True, exist_ok=True) - - _ignore_patterns = shutil.ignore_patterns( - ".git", "__pycache__", "*.pyc", "*.pyo", - "*.o", "*.a", "*.so", "*.dylib", - "build", "dist", "*.egg-info", - ".mypy_cache", ".pytest_cache", ".ruff_cache", - "result_profiling", "*.lock", - ) - try: - shutil.copytree(str(ascend_path), str(backup_dir), - ignore=_ignore_patterns, symlinks=False) - file_count = sum(1 for _ in backup_dir.rglob("*") if _.is_file()) - print_info(f"Code backup [{label}]: {backup_dir} ({file_count} files)") - - # ── Also record git state snapshot ── - try: - head = run_git(ascend_path, "rev-parse", "HEAD").strip() - branch = run_git(ascend_path, "branch", "--show-current").strip() - status = run_git(ascend_path, "status", "--porcelain").strip() - info = ( - f"# Backup: {label}\n" - f"# Time: {ts}\n" - f"# Branch: {branch}\n" - f"# HEAD: {head}\n" - f"# Uncommitted changes: {'yes' if status else 'none'}\n" - ) - (backup_dir / "_BACKUP_INFO.txt").write_text(info, encoding="utf-8") - except Exception: - pass - - return backup_dir - except Exception as e: - print_warn(f"Could not create code backup [{label}]: {e}") - return None - - def _do_finalize(self): - """Generate patch, summary & print final report. - - Does NOT restore the original branch — the work branch must stay - checked out so push_to_github can push it. Branch restore happens - at the end of push_to_github (or handle_failure). - """ - print_header("Phase Final: Finalize & Summary") - - self._print_workspace_info("Phase Final: Finalize") - - ascend_path = Path(self.state.triton_ascend_path) - - # ── Generate final summary ── - print_section("Generate Final Summary") - final_summary_path = WORKSPACE_DIR / FINAL_SUMMARY_FILE - - # Collect step summaries if available - steps_dir = WORKSPACE_DIR / STEPS_DIR - if self.state.total_steps > 1 and steps_dir.exists(): - summaries = [] - for step in self.state.steps: - step_dir = steps_dir / step["id"] - summary_file = step_dir / EACH_STEP_SUMMARY_FILE - if summary_file.exists(): - summaries.append( - f"## {step['id']}\n\n" - f"{summary_file.read_text(encoding='utf-8').strip()}" - ) - if summaries: - final_summary_path.write_text("\n\n".join(summaries), encoding="utf-8") - else: - final_summary_path.write_text( - f"# Triton-Ascend Upstream Sync\n\n" - f"- **Target**: `{self.state.target_commit[:12]}`\n" - f"- **Steps**: {self.state.total_steps}\n" - f"- **Work branch**: `{self.state.work_branch}`\n" - f"- **Status**: Success\n" - f"- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n", - encoding="utf-8", - ) - else: - step_dir = WORKSPACE_DIR / "step-0" - last_summary_path = step_dir / EACH_STEP_SUMMARY_FILE - if last_summary_path.exists(): - shutil.copy2(last_summary_path, final_summary_path) - else: - final_summary_path.write_text( - f"# Triton-Ascend Upstream Sync\n\n" - f"- **Target**: `{self.state.target_commit[:12]}`\n" - f"- **Work branch**: `{self.state.work_branch}`\n" - f"- **Status**: Success\n" - f"- **Upstream commits merged**: {self.state.upstream_commits_count}\n" - f"- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n", - encoding="utf-8", - ) - - print_info(f"Final summary: {final_summary_path}") - - # ── Generate cumulative patch (from original ascend HEAD to latest) ── - try: - patch = run_git(ascend_path, "diff", self.state.ascend_head, "HEAD") - patch_path = WORKSPACE_DIR / FINAL_TARGET_PATCH_FILE - patch_path.write_text(patch, encoding="utf-8") - print_info(f"Cumulative patch: {patch_path} ({len(patch)} bytes)") - except Exception as e: - print_warn(f"Could not generate final patch: {e}") - - # ── Backup work branch code ── - self._backup_code_state("final") - - self.state.summary_rows.append( - ("Finalize", "PASS", f"{self.state.total_steps} step(s) completed") - ) - - # ── Print final summary table ── - print_header("Sync Complete — Success!") - print_elapsed_total() - # Add IR loop metrics if applicable - if self.state.llvm_hash_changed: - self.state.summary_rows.append( - ("IR Loop", "PASS", - f"{len(self.state.ir_loop_details)} iteration(s)")) - # Add pytest result - pytest_status = "PASS" if self.state.pytest_passed else "N/A" - self.state.summary_rows.append(("Pytest", pytest_status, "")) - self.state.summary_rows.append(("OVERALL", "PASS", f"{self.state.total_steps} step(s) completed")) - print_summary_table(self.state.summary_rows) - - # ── Generate sync report ── - self._write_sync_report() - - print_section("Output Files") - for f in sorted(WORKSPACE_DIR.rglob("*")): - if f.is_file() and ".git" not in str(f): - print(f" {f.relative_to(WORKSPACE_DIR)}") - print_info(f"Work branch preserved: {self.state.work_branch}") - print_info(f"To inspect: cd {ascend_path} && git checkout {self.state.work_branch}") - - def _write_sync_report(self) -> None: - """Generate SYNC_REPORT.md via AI — let Claude Code write the report. - - Collects all sync data (fix attempt details, step summaries, error logs, - modified files) into a context file, then calls the AI backend to produce - a comprehensive, human-readable sync report. - """ - report_path = WORKSPACE_DIR / "SYNC_REPORT.md" - - # ── Collect context for AI ── - context = self._build_report_context() - context_path = WORKSPACE_DIR / "report-context.json" - context_path.write_text( - json.dumps(context, indent=2, ensure_ascii=False), encoding="utf-8" - ) - print_info(f"Report context written to {context_path}") - - # ── Build report prompt ── - prompt = self._build_report_prompt(context) - - # ── Call AI backend to generate the report ── - try: - from TA_main2main_workflow.agent.opencode_adapter import ( - _detect_backend, - ) - backend = _detect_backend() - print_info(f"AI backend for report: {backend}") - - # Write prompt file for debugging - prompt_path = WORKSPACE_DIR / "report-prompt.txt" - prompt_path.write_text(prompt, encoding="utf-8") - - print_header("AI Report Generation") - print_info("Calling AI backend to generate sync report...") - - ai_result = run_opencode_adapter({ - "step_id": "sync-report", - "previous_step_id": "", - "previous_step_summary_path": "", - "is_last_step": "true", - "step_index": "final", - "step_dir": str(WORKSPACE_DIR), - "fix_dir": str(WORKSPACE_DIR / "report-fix"), - "conflict_dir": "", - "ascend_path": self.state.triton_ascend_path, - "triton_path": self.state.triton_path, - "reference_dir": _REFERENCE_DIR, - "mode": "report", - "error_logs": json.dumps([str(context_path)], ensure_ascii=False), - "target_commit": self.state.target_commit, - }) - - # AI writes report to step_dir/step_summary.md; we read it from there. - # (ai_result return value is not used directly — report is file-based.) - _ = ai_result # suppress unused-var warning - ai_report_path = WORKSPACE_DIR / EACH_STEP_SUMMARY_FILE - if ai_report_path.exists(): - report_content = ai_report_path.read_text(encoding="utf-8") - # Add metadata header - header = ( - f"# Triton-Ascend Upstream Sync Report\n\n" - f"- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n" - f"- **Target commit**: `{self.state.target_commit[:12]}`\n" - f"- **Work branch**: `{self.state.work_branch}`\n" - f"- **Upstream commits**: {self.state.upstream_commits_count}\n" - f"- **Steps**: {self.state.total_steps}\n" - f"- **Merge conflicts resolved**: {self.state.conflict_files_resolved}\n" - f"- **Build errors fixed**: {sum(s['build_fixes'] for s in self.state.step_details)}\n" - f"- **Test failures fixed**: {sum(s['test_fixes'] for s in self.state.step_details)}\n" - f"- **Total AI fix rounds**: {sum(s['retries'] for s in self.state.step_details)}\n\n" - f"---\n\n" - ) - report_path.write_text(header + report_content, encoding="utf-8") - print_status(True, f"AI-generated sync report: {report_path}") - else: - print_warn("AI did not produce a report — using fallback") - self._write_sync_report_fallback() - except Exception as e: - print_error(f"AI report generation failed: {e}") - print_info("Using fallback report generator...") - self._write_sync_report_fallback() - - def _build_report_context(self) -> dict: - """Collect all sync data into a structured context for AI report generation.""" - total_build_fixes = sum(s["build_fixes"] for s in self.state.step_details) - total_test_fixes = sum(s["test_fixes"] for s in self.state.step_details) - total_retries = sum(s["retries"] for s in self.state.step_details) - - # Collect step AI summaries - step_summaries: dict[str, str] = {} - steps_dir = WORKSPACE_DIR / STEPS_DIR - if steps_dir.exists(): - for step in self.state.steps: - step_dir = steps_dir / step["id"] - parts = [] - for fname in ["analysis.md", "step_summary.md", "review.md"]: - fp = step_dir / fname - if fp.exists(): - parts.append( - f"### {fname}\n\n" - f"{fp.read_text(encoding='utf-8', errors='replace').strip()}" - ) - if parts: - step_summaries[step["id"]] = "\n\n".join(parts) - - return { - "overview": { - "date": time.strftime('%Y-%m-%d %H:%M:%S'), - "target_commit": self.state.target_commit[:12], - "work_branch": self.state.work_branch, - "upstream_commits_count": self.state.upstream_commits_count, - "total_steps": self.state.total_steps, - "conflict_files_resolved": self.state.conflict_files_resolved, - "build_fix_count": total_build_fixes, - "test_fix_count": total_test_fixes, - "total_retries": total_retries, - }, - "step_details": self.state.step_details, - "fix_attempts": self.state.fix_attempts, - "step_ai_summaries": step_summaries, - "step_pr_descriptions": self.state.step_pr_descriptions, - } - - def _build_report_prompt(self, context: dict) -> str: - """Build the AI prompt for generating the sync report.""" - summary_json = json.dumps(context, indent=2, ensure_ascii=False) - return ( - "Generate a comprehensive sync report in Chinese (中文) based on " - "the structured context below. The report should be written as " - "step_summary.md in the output directory.\n\n" - "The report MUST include:\n\n" - "## 1. Executive Summary\n" - "- Brief overview of this sync (how many upstream commits, " - "how many steps, overall outcome)\n" - "- Key metrics (conflicts resolved, build errors fixed, " - "test failures fixed, AI fix rounds)\n\n" - "## 2. Per-Step Analysis\n" - "- For each step, explain:\n" - " - Which upstream commits were merged and what areas they touched\n" - " - What merge conflicts arose and how they were resolved\n" - " - What build errors occurred, root causes, and how AI fixed them\n" - " - What test failures occurred, root causes, and how AI fixed them\n" - "- Include specific file paths and error messages where relevant\n\n" - "## 3. Fix Pattern Analysis\n" - "- Identify recurring patterns across fixes (e.g., API changes, " - "missing includes, signature mismatches)\n" - "- Highlight any fixes that required multiple attempts\n\n" - "## 4. Recommendations\n" - "- Suggest preventative measures for future syncs\n" - "- Flag any areas of the codebase that are particularly fragile\n\n" - "Rules:\n" - "- Write in Chinese (中文)\n" - "- Be specific — include file paths, error messages, commit ranges\n" - "- Write the output to {step_dir}/step_summary.md\n" - "- DO NOT modify any source code — this is a report-only task\n\n" - f"CONTEXT DATA:\n\n{summary_json}" - ) - - def _write_sync_report_fallback(self) -> None: - """Fallback: assemble report from template (no AI).""" - report_path = WORKSPACE_DIR / "SYNC_REPORT.md" - L: list[str] = [] - - total_build_fixes = sum(s["build_fixes"] for s in self.state.step_details) - total_test_fixes = sum(s["test_fixes"] for s in self.state.step_details) - total_retries = sum(s["retries"] for s in self.state.step_details) - - L.append("# Triton-Ascend Upstream Sync Report\n") - L.append(f"**Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}") - L.append(f"**Target commit**: `{self.state.target_commit[:12]}`") - L.append(f"**Work branch**: `{self.state.work_branch}`") - L.append(f"**Status**: Success\n") - - L.append("## Summary\n") - L.append("| Metric | Count |") - L.append("|--------|-------|") - L.append(f"| Upstream commits synced | {self.state.upstream_commits_count} |") - L.append(f"| Steps | {self.state.total_steps} |") - L.append(f"| Merge conflicts resolved | {self.state.conflict_files_resolved} |") - L.append(f"| Build errors fixed | {total_build_fixes} |") - L.append(f"| Test failures fixed | {total_test_fixes} |") - L.append(f"| AI fix rounds | {total_retries} |") - - if self.state.step_details: - L.append("\n## Per-Step Breakdown\n") - L.append("| Step | Commits | Lines | Conflicts | Build Fixes | Test Fixes | Retries |") - L.append("|------|---------|-------|-----------|-------------|------------|---------|") - for s in self.state.step_details: - L.append( - f"| {s['step_id']} ({s['step_index']}/{self.state.total_steps}) " - f"| {s['commits']} | {s['source_lines']} | {s['conflict_files']} " - f"| {s['build_fixes']} | {s['test_fixes']} | {s['retries']} |" - ) - - for fa in self.state.fix_attempts: - ftype = fa["fix_type"].upper() - L.append( - f"\n### {fa['step_id']} — Fix {fa['attempt']} ({ftype})\n" - ) - if fa["modified_files"]: - L.append(f"**Files**: {', '.join(f'`{f}`' for f in fa['modified_files'])}") - ai_sum = fa.get("ai_summary", "").strip() - if ai_sum: - L.append(f"\n{ai_sum}") - - steps_dir = WORKSPACE_DIR / STEPS_DIR - if steps_dir.exists(): - for step in self.state.steps: - step_dir = steps_dir / step["id"] - for fname in ["analysis.md", "step_summary.md", "review.md"]: - fp = step_dir / fname - if fp.exists(): - L.append( - f"\n### {step['id']} — {fname}\n\n" - f"{fp.read_text(encoding='utf-8', errors='replace').strip()}\n" - ) - - L.append(f"\n---\n🤖 Generated at {time.strftime('%Y-%m-%d %H:%M:%S')}\n") - report_path.write_text("\n".join(L), encoding="utf-8") - print_info(f"Fallback sync report: {report_path}") - - # ═══════════════════════════════════════════════════════════════════════════ - # Terminal nodes (routed from execute_sync) - # ═══════════════════════════════════════════════════════════════════════════ - - @listen(UpgradeCompleted) - def push_to_github(self): - """Push work branch & create a single GitHub PR after ALL steps complete. - - In the vllm-ascend step-by-step merge style, all step commits accumulate - on the work branch locally. Only after every step passes (merge → - resolve → build → test → fix → commit) do we push and open one PR. - """ - if os.getenv("PUSH_TO_GITHUB", "false").lower() != "true": - print_info("PUSH_TO_GITHUB is not 'true' — skipping PR creation") - print_info("To push manually:") - print_info(f" cd {self.state.triton_ascend_path}") - print_info(f" git checkout {self.state.work_branch}") - print_info(f" git push -u origin {self.state.work_branch}") - self.state.summary_rows.append(("Push & PR", "SKIP", "PUSH_TO_GITHUB not set")) - return "SKIP_PUSH" - - print_header("Push to GitHub & Create PR") - - self._print_workspace_info("Push to GitHub & Create PR") - - github_repo = os.getenv("GITHUB_REPO", "triton-lang/triton-ascend") - if not github_repo: - print_error("GITHUB_REPO is empty — cannot create PR") - self.state.summary_rows.append(("Push & PR", "FAIL", "GITHUB_REPO empty")) - self.state.final_status = UpgradeFailed - return UpgradeFailed - - # ── Build a comprehensive PR body from step summaries ── - pr_body_path = WORKSPACE_DIR / FINAL_SUMMARY_FILE - self._build_pr_body(pr_body_path) - - # ── Push AscendNPU-IR submodule first ── - self._push_submodule_if_needed() - - try: - pr_url = push_and_create_pr( - ascend_path=Path(self.state.triton_ascend_path), - github_repo=github_repo, - work_branch=self.state.work_branch, - summary_path=pr_body_path, - target_commit=self.state.target_commit, - ) - self.state.pr_url = pr_url - print_status(True, f"PR created: {pr_url}") - self.state.summary_rows.append(("Push & PR", "PASS", pr_url)) - except Exception as e: - print_error(f"Failed to push/create PR: {e}") - # ── Print detailed failure diagnostics ── - if isinstance(e, subprocess.CalledProcessError): - print_section("Push/PR Failure Details") - print_key_value("Command", " ".join(e.cmd) if e.cmd else "N/A") - print_key_value("Exit code", str(e.returncode)) - if e.stdout: - print_info(f"stdout:\n{e.stdout.strip()}") - if e.stderr: - print_error(f"stderr:\n{e.stderr.strip()}") - else: - import traceback - print_info(f"Traceback:\n{traceback.format_exc()}") - # Print git context for debugging - ascend_path = Path(self.state.triton_ascend_path) - print_section("Git Context at Failure") - print_key_value("Work branch", self.state.work_branch) - try: - current_branch = run_git(ascend_path, "branch", "--show-current").strip() - print_key_value("Current branch", current_branch) - status_out = run_git(ascend_path, "status", "--short").strip() - print_info(f"Git status:\n{status_out}" if status_out else "Git status: (clean)") - log_out = run_git(ascend_path, "log", "--oneline", "-5") - print_info(f"Recent commits:\n{log_out.strip()}") - except Exception: - pass - self.state.summary_rows.append(("Push & PR", "FAIL", str(e)[:60])) - self.state.final_status = UpgradeFailed - # Still try to restore branch, then signal failure - self._restore_branch() - return UpgradeFailed - - # ── Restore original branch after push ── - self._restore_branch() - return self.state.pr_url if self.state.pr_url else "SKIP_PUSH" - - def _restore_branch(self) -> None: - """Restore the original branch after all work is done.""" - ascend_path = Path(self.state.triton_ascend_path) - print_section("Restore Original Branch") - try: - current = run_git(ascend_path, "branch", "--show-current").strip() - if current != self.state.original_branch: - run_git(ascend_path, "checkout", self.state.original_branch) - print_status(True, f"Restored to '{self.state.original_branch}'") - else: - print_info(f"Already on '{self.state.original_branch}'") - except Exception as e: - print_warn(f"Could not restore branch: {e}") - print_info(f"Work branch '{self.state.work_branch}' left checked out") - - def _build_pr_body(self, output_path: Path) -> None: - """Build a comprehensive PR body from all step descriptions and summaries.""" - parts: list[str] = [] - - # Title / overview - parts.append( - "# Triton-Ascend Upstream Sync\n\n" - f"- **Target commit**: `{self.state.target_commit[:12]}`\n" - f"- **Work branch**: `{self.state.work_branch}`\n" - f"- **Steps completed**: {self.state.total_steps}\n" - f"- **Upstream commits merged**: {self.state.upstream_commits_count}\n" - f"- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n" - ) - - # Per-step progress - if self.state.step_pr_descriptions: - parts.append("## Step Progress\n") - for desc in self.state.step_pr_descriptions: - parts.append(f"- {desc}\n") - - # Per-step AI summaries (if available) - steps_dir = WORKSPACE_DIR / STEPS_DIR - if self.state.total_steps > 1 and steps_dir.exists(): - parts.append("\n## Step Details\n") - for step in self.state.steps: - step_dir = steps_dir / step["id"] - summary_file = step_dir / EACH_STEP_SUMMARY_FILE - if summary_file.exists(): - parts.append( - f"### {step['id']}\n\n" - f"{summary_file.read_text(encoding='utf-8').strip()}\n\n" - ) - else: - parts.append( - f"### {step['id']}\n\n" - f"- Commits: {step['commit_count']}\n" - f"- End commit: `{step['end_commit'][:12]}`\n" - f"- Source lines changed: {step.get('source_changed_lines', '?')}\n\n" - ) - elif steps_dir.exists(): - # Single step: include its summary - step_dir = WORKSPACE_DIR / "step-0" - summary_file = step_dir / EACH_STEP_SUMMARY_FILE - if summary_file.exists(): - parts.append( - "\n## Summary\n\n" - f"{summary_file.read_text(encoding='utf-8').strip()}\n" - ) - else: - # Fallback: just the final summary - fallback = WORKSPACE_DIR / FINAL_SUMMARY_FILE - if fallback.exists(): - parts.append(fallback.read_text(encoding='utf-8')) - - parts.append( - f"\n---\n" - f"🤖 Generated with [TA_main2main_workflow]" - f"(https://github.com/TecJesh/TA-AI-WorkFlow)" - f" at {time.strftime('%Y-%m-%d %H:%M:%S')}\n" - ) - - output_path.write_text("".join(parts), encoding="utf-8") - print_info(f"PR body written to {output_path}") - - @listen(UpgradeFailed) - def handle_failure(self): - """write FAILURE.md, print diagnostics & summary, suggest recovery commands.""" - print_header("Sync Failed — Diagnostics") - - self._print_workspace_info("Handle Failure") - - ascend_path = Path(self.state.triton_ascend_path) - - # ── Backup code state BEFORE anything else ── - # Capture the working tree so AI fixes, conflict resolutions, and - # partial merge progress are preserved as CI artifacts even on failure. - failed_step = self.state.current_step + 1 if self.state.current_step < self.state.total_steps else self.state.total_steps - self._backup_code_state(f"failed-step{failed_step}") - - print_error(f"Upgrade failed after {self.state.retry_count} retries") - - print_section("Failure Details") - print_key_value("Target commit", self.state.target_commit[:12]) - print_key_value("Work branch", self.state.work_branch) - print_key_value("Original branch", self.state.original_branch) - print_key_value("Conflict files", ", ".join(self.state.conflict_files) if self.state.conflict_files else "none") - print_key_value("Build passed", str(self.state.build_passed)) - print_key_value("Test passed", str(self.state.test_passed)) - - failure_path = WORKSPACE_DIR / "FAILURE.md" - failure_text = ( - f"# Upgrade Failed\n\n" - f"- **Target**: `{self.state.target_commit[:12]}`\n" - f"- **Work branch**: `{self.state.work_branch}`\n" - f"- **Original branch**: `{self.state.original_branch}`\n" - f"- **Retries**: {self.state.retry_count}/{self.state.max_retries}\n" - f"- **Conflict files**: {', '.join(self.state.conflict_files) if self.state.conflict_files else 'none'}\n" - f"- **Build passed**: {self.state.build_passed}\n" - f"- **Test passed**: {self.state.test_passed}\n" - f"- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n" - f"## Recovery\n\n" - f"```bash\n" - f"cd {ascend_path}\n" - f"git checkout {self.state.original_branch}\n" - f"# Work branch '{self.state.work_branch}' has the partial merge\n" - f"# git branch -D {self.state.work_branch}\n" - f"```\n" - ) - failure_path.write_text(failure_text, encoding="utf-8") - print_info(f"Failure report: {failure_path}") - - print_elapsed_total() - self.state.summary_rows.append(("OVERALL", "FAIL", f"Failed after {self.state.retry_count} retries")) - print_summary_table(self.state.summary_rows) - - print_section("Recovery") - print_info(f"Work branch '{self.state.work_branch}' preserved for manual inspection") - print_info(f"To restore: cd {ascend_path} && git checkout {self.state.original_branch}") - print_info(f"To clean up: cd {ascend_path} && git branch -D {self.state.work_branch}") - - self.state.final_status = UpgradeFailed - return UpgradeFailed + log.error(f"Failed to create PR: {e}") diff --git a/src/TA_main2main_workflow/main.py b/src/TA_main2main_workflow/main.py index d735cf2..616aa73 100644 --- a/src/TA_main2main_workflow/main.py +++ b/src/TA_main2main_workflow/main.py @@ -1,193 +1,78 @@ #!/usr/bin/env python3 -"""CLI entrypoint for TA_main2main_workflow — Triton-Ascend upstream sync. - -Commands: - ta-kickoff Run the main2main sync flow (all output printed locally) - ta-plot Generate a flow diagram (HTML) - -Environment variables: - TRITON_ASCEND_PATH — path to triton-ascend repo (default: cwd) - TRITON_PATH — path to upstream triton repo (default: uses remote) - TRITON_TARGET_COMMIT — specific upstream commit to sync to (default: HEAD) - AI_BACKEND — "opencode" or "claude" (default: auto-detect) - SKIP_AI_ANALYSIS — set to "true" to skip AI (NOT recommended) - SKIP_BUILD — set to "true" to skip build step - SKIP_E2E_TEST — set to "true" to skip test step - PUSH_TO_GITHUB — set to "true" to auto-create PR after success - GITHUB_REPO — "owner/repo" for PR creation - LLVM_INSTALL_PREFIX — path to LLVM for building - CONDA_ENV — conda env name (default: ta-upgrade) - NUM_PROCS — number of parallel pytest workers (default: 16) - - TA_MODE — Execution mode: - full (default) Complete flow: merge → build → test → fix → PR - merge Merge + AI resolve only, then push work branch & exit. - Used by CI: runs on ubuntu-latest, then triggers NPU tests. - fix AI fix only on an existing work branch. Requires: - TA_WORK_BRANCH — work branch name - TA_ERROR_LOGS_PATH — path to test failure logs (optional) - TA_FIX_ATTEMPT — retry attempt number (optional) -""" +"""CLI entrypoint for TA_main2main_workflow — Triton-Ascend upstream sync.""" import argparse -import os import sys -from pathlib import Path from TA_main2main_workflow.flow import TA_Main2MainFlow +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.logging import get_logger from TA_main2main_workflow.utils import UpgradeFailed - -def _print_startup_banner() -> None: - skip_ai = os.getenv("SKIP_AI_ANALYSIS", "false").lower() == "true" - skip_build = os.getenv("SKIP_BUILD", "false").lower() == "true" - skip_test = os.getenv("SKIP_E2E_TEST", "false").lower() == "true" - ai_backend = os.getenv("AI_BACKEND", "auto-detect") - mode = os.getenv("TA_MODE", "full") - - print(f"╔{'═' * 60}╗") - print(f"║ TA_main2main_workflow — Triton-Ascend Upstream Sync ║") - print(f"╠{'═' * 60}╣") - print(f"║ Mode: {mode:<44}║") - print(f"║ AI Backend: {ai_backend:<44}║") - print(f"║ AI Enabled: {'YES' if not skip_ai else 'NO (SKIP_AI_ANALYSIS=true)':<44}║") - print(f"║ Skip Build: {str(skip_build):<44}║") - print(f"║ Skip Test: {str(skip_test):<44}║") - print(f"╚{'═' * 60}╝") - - if skip_ai: - print() - print(" ⚠ WARNING: SKIP_AI_ANALYSIS=true") - print(" ⚠ AI will NOT be called to resolve conflicts or fix failures!") - print(" ⚠ You must resolve conflicts and fix test failures manually.") - print() - - -def _is_failed(result) -> bool: - """Check whether a kickoff result indicates workflow failure. - - Handles both plain string returns (merge/fix modes) and CrewAI - CrewOutput objects (full mode). - """ - if result is None: - return False - if isinstance(result, str): - return result == UpgradeFailed - # CrewAI CrewOutput / object with raw attribute - if hasattr(result, 'raw'): - return str(result.raw) == UpgradeFailed - # Last resort: string representation - return str(result) == UpgradeFailed +log = get_logger(__name__) def kickoff(): parser = argparse.ArgumentParser( - description="Triton-Ascend Main2Main Upstream Sync Flow" + description="Triton-Ascend Main2Main Upstream Sync" ) parser.add_argument( - "--mode", default=None, - choices=["full", "merge", "fix"], - help="Execution mode: full (default), merge (merge+resolve only), " - "fix (AI fix on existing work branch). " - "Can also be set via TA_MODE env var." + "--triton-ascend-path", default=None, help="Path to triton-ascend repo" ) parser.add_argument( - "--work-branch", default=None, - help="Work branch name (required for --mode=fix). " - "Can also be set via TA_WORK_BRANCH env var." + "--triton-path", default=None, help="Path to local triton repo (offline mode)" ) parser.add_argument( - "--error-logs-path", default=None, - help="Path to test failure logs for AI fix (--mode=fix). " - "Can also be set via TA_ERROR_LOGS_PATH env var." + "--target-commit", default=None, help="Upstream commit SHA to merge" ) + parser.add_argument("--llvm-prefix", default=None, help="LLVM install prefix path") parser.add_argument( - "--fix-attempt", type=int, default=None, - help="Retry attempt number (--mode=fix). " - "Can also be set via TA_FIX_ATTEMPT env var." + "--build-procs", type=int, default=None, help="Parallel build workers" ) parser.add_argument( - "--triton-ascend-path", default=None, - help="Local path to the triton-ascend repository (default: current directory)" - ) - parser.add_argument( - "--triton-path", default=None, - help="Local path to the upstream triton repository (default: uses remote)" - ) - parser.add_argument( - "--target-commit", default=None, - help="Upstream triton commit SHA to merge (default: upstream HEAD)" - ) - parser.add_argument( - "--llvm-prefix", default=None, - help="LLVM install prefix path for building" - ) - parser.add_argument( - "--conda-env", default=None, - help="Conda environment name (default: ta-upgrade)" - ) - parser.add_argument( - "--num-procs", type=int, default=None, - help="Number of parallel pytest workers (default: 16)" + "--test-procs", type=int, default=None, help="Parallel pytest workers" ) args = parser.parse_args() - # ── Mode: CLI arg takes precedence over env var ── - if args.mode: - os.environ["TA_MODE"] = args.mode - if args.work_branch: - os.environ["TA_WORK_BRANCH"] = args.work_branch - if args.error_logs_path: - os.environ["TA_ERROR_LOGS_PATH"] = args.error_logs_path - if args.fix_attempt is not None: - os.environ["TA_FIX_ATTEMPT"] = str(args.fix_attempt) - - _print_startup_banner() - - inputs = {} + config = TAConfig.from_env() if args.triton_ascend_path: - inputs["triton_ascend_path"] = args.triton_ascend_path + config.triton_ascend_path = args.triton_ascend_path if args.triton_path: - inputs["triton_path"] = args.triton_path + config.triton_path = args.triton_path if args.target_commit: - inputs["target_commit"] = args.target_commit + config.target_commit = args.target_commit if args.llvm_prefix: - inputs["llvm_prefix"] = args.llvm_prefix - if args.conda_env: - inputs["conda_env"] = args.conda_env - if args.num_procs: - inputs["num_procs"] = args.num_procs + config.llvm_install_prefix = args.llvm_prefix + if args.build_procs is not None: + config.build_procs = args.build_procs + if args.test_procs is not None: + config.test_procs = args.test_procs - flow = TA_Main2MainFlow() + _print_banner(config) + + flow = TA_Main2MainFlow(config=config) try: - result = flow.kickoff(inputs=inputs if inputs else None) + result = flow.run() except Exception as exc: - print(f"\n{'=' * 60}") - print(f" WORKFLOW CRASHED: {exc}") - print(f"{'=' * 60}") + log.error(f"WORKFLOW CRASHED: {exc}") sys.exit(1) - if _is_failed(result): - print(f"\n{'=' * 60}") - print(f" WORKFLOW FAILED — exiting with code 1") - print(f"{'=' * 60}") + if result == UpgradeFailed: + log.error("WORKFLOW FAILED") sys.exit(1) - print(f"\n{'=' * 60}") - print(f" WORKFLOW COMPLETED SUCCESSFULLY") - print(f"{'=' * 60}") - + log.info("WORKFLOW COMPLETED SUCCESSFULLY") -def plot(): - import shutil - output_dir = Path(__file__).resolve().parent / "output" - output_dir.mkdir(parents=True, exist_ok=True) - flow = TA_Main2MainFlow() - tmp_html = Path(flow.plot(filename="flow.html", show=False)) - for f in tmp_html.parent.iterdir(): - shutil.copy2(f, output_dir / f.name) - print(f"Flow plot saved to: {output_dir / tmp_html.name}") +def _print_banner(config: TAConfig) -> None: + ai = "NO (SKIP_AI_ANALYSIS=true)" if config.skip_ai_analysis else "YES" + log.header("TA_main2main_workflow — Triton-Ascend Upstream Sync") + log.key_value("AI Backend", config.ai_backend) + log.key_value("AI Enabled", ai) + log.key_value("Skip Build", str(config.skip_build)) + log.key_value("Skip Test", str(config.skip_e2e_test)) + if config.skip_ai_analysis: + log.warning("SKIP_AI_ANALYSIS=true — AI will not be called!") if __name__ == "__main__": diff --git a/src/TA_main2main_workflow/pipeline/__init__.py b/src/TA_main2main_workflow/pipeline/__init__.py new file mode 100644 index 0000000..3a2b64a --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/__init__.py @@ -0,0 +1,9 @@ +"""Pipeline step functions. + +Each step is an independent function with signature:: + + def step_xxx(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext + +Steps read from *ctx*, perform their work, and return an updated +``WorkflowContext`` (never mutating the input). +""" diff --git a/src/TA_main2main_workflow/pipeline/ai_fix.py b/src/TA_main2main_workflow/pipeline/ai_fix.py new file mode 100644 index 0000000..abe53b5 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/ai_fix.py @@ -0,0 +1,412 @@ +"""AI fix pipeline step — spawns opencode to resolve build/test failures. + +Also used by the merge conflict resolution step via :func:`run_opencode_adapter`. +""" + +from __future__ import annotations + +import json +import os +import queue +import subprocess +import threading +import time +from pathlib import Path +from typing import Any + +from pydantic import BaseModel, Field + +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils import STEPS_DIR, WORKSPACE_DIR + +log = get_logger(__name__) +_REF = str(Path(__file__).parent.parent / "reference") +_PROMPT_DIR = Path(__file__).parent.parent / "agent" + +_PROMPT_FILES: dict[str, str] = { + "conflict": "prompt_conflict.md", + "build_fix": "prompt_build_fix.md", + "test_fix": "prompt_test_fix.md", +} + +_TIMEOUT_MINUTES = 30 +_STALE_SECONDS = 1200 + +_MODE_LABELS: dict[str, str] = { + "conflict": "CONFLICT RESOLUTION", + "build_fix": "BUILD FIX", + "test_fix": "TEST FIX", +} + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AIResult +# ═══════════════════════════════════════════════════════════════════════════════ + + +class AIResult(BaseModel): + modified_files: list[str] = Field(default_factory=list) + is_noop: bool = Field(default=False) + step_summary: str = Field(default="") + elapsed_seconds: float = Field(default=0.0) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# pipeline entry point +# ═══════════════════════════════════════════════════════════════════════════════ + + +def ai_fix( + ctx: WorkflowContext, config: TAConfig, attempt: int = 1, mode: str = "build_fix" +) -> WorkflowContext: + """AI fix step — called by build and test phases on failure.""" + if config.skip_ai_analysis: + log.info("SKIP_AI_ANALYSIS=true — skipping AI fix") + return ctx + ascend_path = Path(ctx.triton_ascend_path) + step = ctx.steps[ctx.current_step] if ctx.current_step < len(ctx.steps) else None + step_id = step["id"] if step else "step-0" + step_dir = WORKSPACE_DIR / STEPS_DIR / step_id + step_dir.mkdir(parents=True, exist_ok=True) + + log.step(attempt, config.max_retries, "AI fix") + try: + result = run_opencode_adapter( + { + "step_id": f"{step_id}-fix-{attempt}", + "step_dir": str(step_dir), + "ascend_path": str(ascend_path), + "triton_path": ctx.triton_ascend_path, + "reference_dir": _REF, + "mode": mode, + "error_logs": json.dumps(ctx.fix_errors, ensure_ascii=False), + "target_commit": ctx.target_commit, + "step_index": f"{ctx.current_step + 1}/{ctx.total_steps}", + } + ) + log.ai_result( + bool(result.modified_files), + result.modified_files, + (result.step_summary or "")[:500], + ) + return ctx + except Exception as e: + log.error(f"AI fix failed: {e}") + return ctx + + +# ═══════════════════════════════════════════════════════════════════════════════ +# opencode adapter +# ═══════════════════════════════════════════════════════════════════════════════ + + +def run_opencode_adapter(inputs: dict[str, Any]) -> AIResult: + """Run opencode for conflict resolution or test/build fixing.""" + mode = inputs.get("mode", "unknown") + step_id = inputs.get("step_id", "?") + mode_label = _MODE_LABELS.get(mode, mode.upper()) + + log.info(f"{'═' * 60}") + log.info(f" {mode_label}") + log.info(f" Step: {step_id}") + log.info(f" Time: {time.strftime('%H:%M:%S')}") + log.info(f"{'═' * 60}") + + t0 = time.monotonic() + result = _run_opencode(inputs) + result.elapsed_seconds = time.monotonic() - t0 + + icon = "✔" if result.modified_files else "○" + log.info(f" {icon} AI task completed in {result.elapsed_seconds:.1f}s") + if result.modified_files: + log.info(f" Modified: {', '.join(result.modified_files)}") + if result.is_noop: + log.info(f" (no changes needed)") + + return result + + +def _run_opencode(inputs: dict[str, Any]) -> AIResult: + """Run opencode with JSONL streaming and stale/total timeout protection.""" + prompt = _build_prompt(inputs) + step_dir = inputs.get("step_dir", "") + step_path = Path(step_dir) if step_dir else None + log_path = step_path / "opencode.log" if step_path else None + raw_path = step_path / "opencode_raw.jsonl" if step_path else None + stderr_path = step_path / "opencode_stderr.log" if step_path else None + + for p in (log_path, raw_path, stderr_path): + if p: + p.write_text("") + + _print_prompt(prompt) + if log_path: + _log_prompt(prompt, log_path) + + lines, stop_reason = _run_opencode_once(prompt, log_path, raw_path, stderr_path) + + if stop_reason and stderr_path and stderr_path.exists(): + stderr_content = stderr_path.read_text(encoding="utf-8", errors="replace")[ + -2000: + ] + if stderr_content: + log.info(f"[opencode] stderr tail:\n{stderr_content}") + + result = _build_result(step_path, inputs.get("ascend_path", ""), "".join(lines)) + if stop_reason and not result.step_summary: + result.step_summary = f"opencode stopped due to {stop_reason}" + return result + + +def _run_opencode_once( + prompt: str, + log_path: Path | None, + raw_path: Path | None, + stderr_path: Path | None, +) -> tuple[list[str], str | None]: + """Launch opencode, stream JSONL output, enforce timeout. Returns (lines, stop_reason).""" + stderr_fh = stderr_path.open("a", encoding="utf-8") if stderr_path else None + proc = subprocess.Popen( + [ + "opencode", + "run", + "--format", + "json", + "--dangerously-skip-permissions", + prompt, + ], + stdout=subprocess.PIPE, + stderr=stderr_fh or subprocess.DEVNULL, + text=True, + bufsize=1, + env=_subprocess_env(), + ) + + lines_queue: queue.Queue[str | None] = queue.Queue() + + def _stdout_reader() -> None: + assert proc.stdout is not None + for line in proc.stdout: + lines_queue.put(line) + lines_queue.put(None) + + reader_thread = threading.Thread(target=_stdout_reader, daemon=True) + reader_thread.start() + + state = _EventState() + log_fh = log_path.open("a", encoding="utf-8") if log_path else None + raw_fh = raw_path.open("a", encoding="utf-8") if raw_path else None + + deadline = time.monotonic() + _TIMEOUT_MINUTES * 60 + last_output_time = time.monotonic() + stop_reason: str | None = None + + try: + while True: + try: + line = lines_queue.get(timeout=1.0) + except queue.Empty: + now = time.monotonic() + if now > deadline: + log.info( + f"[opencode] TOTAL TIMEOUT ({_TIMEOUT_MINUTES}min), killing process" + ) + proc.kill() + stop_reason = "total_timeout" + break + if now - last_output_time > _STALE_SECONDS: + log.info( + f"[opencode] STALE TIMEOUT ({_STALE_SECONDS}s no output), killing process" + ) + proc.kill() + stop_reason = "stale_timeout" + break + continue + + if line is None: + break + + last_output_time = time.monotonic() + state.lines.append(line) + if raw_fh: + raw_fh.write(line) + _print_opencode_event(line, state) + if log_fh: + _log_opencode_event(line, log_fh) + finally: + if log_fh: + log_fh.close() + if raw_fh: + raw_fh.close() + if stderr_fh: + stderr_fh.close() + + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + stop_reason = stop_reason or "total_timeout" + proc.wait(timeout=10) + + return state.lines, stop_reason + + +class _EventState: + def __init__(self) -> None: + self.lines: list[str] = [] + self._tool_by_call: dict[str, str] = {} + self._line_count: int = 0 + + +def _print_opencode_event(line: str, state: _EventState) -> None: + try: + ev = json.loads(line) + except json.JSONDecodeError: + return + + t = ev.get("type") + part = ev.get("part", {}) + + if t == "text": + text = part.get("text", "") + if text: + print(text, end="", flush=True) + state._line_count += text.count("\n") + + elif t == "tool_use": + tool = part.get("tool", "") + call_id = part.get("callID", "") + st = part.get("state", {}) + status = st.get("status", "") + inp = st.get("input", {}) + + if status == "pending": + state._tool_by_call[call_id] = tool + brief = json.dumps(inp, ensure_ascii=False)[:200] + print(f"\n > [AI: {tool}] {brief}", flush=True) + + elif status == "completed": + output = st.get("output", "") + if output: + display = ( + output + if len(output) <= 2000 + else output[:2000] + "\n... [truncated]" + ) + print( + f"\n {'─' * 56}\n [AI output]\n {display}\n {'─' * 56}", + flush=True, + ) + + +def _log_opencode_event(line: str, fh: Any) -> None: + try: + ev = json.loads(line) + except json.JSONDecodeError: + fh.write(line) + return + + t = ev.get("type") + part = ev.get("part", {}) + + if t == "text": + text = part.get("text", "") + if text: + fh.write(text) + + elif t == "tool_use": + tool = part.get("tool", "") + st = part.get("state", {}) + inp = json.dumps(st.get("input", {}), ensure_ascii=False) + fh.write(f"\n[AI: {tool}] <- {inp[:500]}\n") + output = st.get("output", "") + if output: + fh.write(f"{'─' * 60}\n[output]\n{output[:4000]}\n{'─' * 60}\n") + + fh.flush() + + +# ═══════════════════════════════════════════════════════════════════════════════ +# helpers +# ═══════════════════════════════════════════════════════════════════════════════ + + +def _build_prompt(inputs: dict[str, Any]) -> str: + from collections import defaultdict + + mode = inputs.get("mode", "build_fix") + prompt_file = _PROMPT_FILES.get(mode, "prompt_build_fix.md") + template = (_PROMPT_DIR / prompt_file).read_text(encoding="utf-8") + ctx = defaultdict(str, {k: str(v) for k, v in inputs.items()}) + return template.format_map(ctx) + + +def _print_prompt(prompt: str) -> None: + log.info(f"{'━' * 60}") + log.info(f" AI TASK PROMPT") + log.info(f"{'━' * 60}") + if len(prompt) > 8000: + print(prompt[:4000]) + print( + f"\n... [{len(prompt) - 8000} chars truncated, see log for full prompt] ...\n" + ) + print(prompt[-4000:]) + else: + print(prompt) + log.info(f"{'━' * 60}") + + +def _log_prompt(prompt: str, log_path: Path) -> None: + with log_path.open("a", encoding="utf-8") as fh: + fh.write(f"{'═' * 60}\nAI TASK PROMPT:\n{'═' * 60}\n{prompt}\n{'═' * 60}\n\n") + + +def _subprocess_env() -> dict: + """Environment for launching opencode. + + Sets IS_SANDBOX=1 when running as root (CI containers) to allow + --dangerously-skip-permissions. + """ + env = os.environ.copy() + if hasattr(os, "geteuid") and os.geteuid() == 0: + env.setdefault("IS_SANDBOX", "1") + return env + + +def _build_result( + step_dir: Path | None, ascend_path: str, output_text: str +) -> AIResult: + """Build AIResult from AI output: extract summary, detect modified files.""" + summary = "" + if step_dir: + summary_path = step_dir / "step_summary.md" + if summary_path.exists(): + summary = summary_path.read_text(encoding="utf-8") + + if not summary: + summary = output_text[-4000:] if output_text else "" + + modified_files = _modified_files(ascend_path) + return AIResult( + modified_files=modified_files, + is_noop=not modified_files, + step_summary=summary, + ) + + +def _modified_files(ascend_path: str) -> list[str]: + if not ascend_path: + return [] + try: + result = subprocess.run( + ["git", "diff", "--name-only", "HEAD"], + cwd=ascend_path, + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError: + return [] + return [line for line in result.stdout.splitlines() if line] diff --git a/src/TA_main2main_workflow/pipeline/build.py b/src/TA_main2main_workflow/pipeline/build.py new file mode 100644 index 0000000..18b3d03 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/build.py @@ -0,0 +1,254 @@ +"""Pipeline step: Build LLVM then Triton-Ascend, each with retry/fix loops.""" + +from __future__ import annotations +import json, os, subprocess +from pathlib import Path +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.tracker import timed +from TA_main2main_workflow.utils.git import run_git +from TA_main2main_workflow.utils import BUILD_RESULT_FILE, STEPS_DIR, WORKSPACE_DIR +from TA_main2main_workflow.pipeline.ai_fix import ai_fix + +log = get_logger(__name__) + + +def build(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Build phase: LLVM → triton-ascend, with unified retry+fix loop. + + Each retry attempt rebuilds LLVM first, then triton-ascend. + This ensures LLVM fixes are re-validated when triton fails. + """ + if config.skip_build: + log.info("SKIP_BUILD=true — skipping build") + return ctx.copy_with(build_passed=True) + + if not config.skip_llvm_rebuild: + ctx = _llvm_setup(ctx, config) + + for attempt in range(config.max_retries + 1): + ctx = ctx.copy_with(retry_count=attempt) + if attempt > 0: + log.header(f"Build Fix Attempt {attempt}/{config.max_retries}") + ctx = ai_fix(ctx, config, attempt=attempt, mode="build_fix") + + # ── Rebuild LLVM every attempt ────────────────────────────── + if not config.skip_llvm_rebuild: + with timed("build-llvm"): + ctx = _build_llvm(ctx, config.build_procs) + if not ctx.build_passed: + log.info(f"LLVM build failed (attempt {attempt + 1}) — retrying") + continue + + # ── Build triton-ascend ───────────────────────────────────── + with timed("build-triton"): + ctx = _build_triton(ctx, config, clean=(attempt == 0)) + if ctx.build_passed: + return ctx + log.info(f"Triton build failed (attempt {attempt + 1}) — retrying") + + return ctx.copy_with(build_passed=False) + + +# ═══════════════════════════════════════════════════════════════════════════ +# LLVM +# ═══════════════════════════════════════════════════════════════════════════ + + +def _llvm_setup(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Clone LLVM, checkout hash, apply patch. Idempotent — only runs if hash changed.""" + ascend_path = Path(ctx.triton_ascend_path) + llvm_project = WORKSPACE_DIR / "llvm-project" + llvm_hash_file = ascend_path / "cmake" / "llvm-hash.txt" + + if not llvm_hash_file.exists(): + log.info("No llvm-hash.txt — skipping LLVM rebuild") + return ctx + required_hash = llvm_hash_file.read_text(encoding="utf-8").strip() + if not required_hash: + return ctx + + if not llvm_project.exists(): + run_git(WORKSPACE_DIR, "clone", config.llvm_repo_url, str(llvm_project)) + + log.section(f"LLVM setup (hash: {required_hash[:12]})") + run_git(llvm_project, "fetch", "origin", required_hash) + run_git(llvm_project, "reset", "--hard", "HEAD") + run_git(llvm_project, "clean", "-fd") + run_git(llvm_project, "checkout", "-f", required_hash) + + patch_dir = ascend_path / "third_party/ascend/patch" + patch_files = sorted(patch_dir.glob("*.patch")) if patch_dir.exists() else [] + if patch_files: + # Patch naming: llvm_patch_.patch + if required_hash[:7] in patch_files[0].name: + log.info(f"Applying patch: {patch_files[0].name}") + run_git(llvm_project, "apply", str(patch_files[0])) + else: + log.info( + f"LLVM hash changed ({required_hash[:12]}), patch {patch_files[0].name} " + f"is for old version — will let AI generate new patch" + ) + return ctx + + +def _build_llvm(ctx: WorkflowContext, num_procs: int = 32) -> WorkflowContext: + """cmake + ninja for LLVM. Pure build — no retry logic. + + On success, regenerates patch if AI fix modified the source. + """ + llvm_project = WORKSPACE_DIR / "llvm-project" + llvm_install = WORKSPACE_DIR / "llvm-install" + ascend_path = Path(ctx.triton_ascend_path) + required_hash = ( + (ascend_path / "cmake" / "llvm-hash.txt").read_text(encoding="utf-8").strip() + ) + + step_id = ctx.steps[ctx.current_step]["id"] if ctx.steps else "step-0" + step_dir = WORKSPACE_DIR / STEPS_DIR / step_id + step_dir.mkdir(parents=True, exist_ok=True) + + build_dir = WORKSPACE_DIR / "llvm-build" + build_dir.mkdir(parents=True, exist_ok=True) + + log.info(f"cmake configure (parallel: {num_procs})...") + cmake_log = step_dir / "llvm-cmake.log" + cmake_err = step_dir / "llvm-cmake.err" + try: + with open(cmake_log, "w") as o, open(cmake_err, "w") as e: + subprocess.run( + [ + "cmake", + str(llvm_project / "llvm"), + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + "-DLLVM_ENABLE_ASSERTIONS=ON", + "-DLLVM_ENABLE_PROJECTS=mlir;llvm;lld", + "-DLLVM_TARGETS_TO_BUILD=host;NVPTX;AMDGPU", + f"-DCMAKE_INSTALL_PREFIX={llvm_install}", + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_CXX_COMPILER=clang++", + ], + cwd=build_dir, + check=True, + stdout=o, + stderr=e, + ) + except subprocess.CalledProcessError: + log.error("LLVM cmake FAILED") + return ctx.copy_with( + build_passed=False, fix_errors=[str(cmake_log), str(cmake_err)] + ) + + log.info(f"ninja -j{num_procs} install (this may take a while)...") + ninja_log = step_dir / "llvm-ninja.log" + ninja_err = step_dir / "llvm-ninja.err" + try: + with open(ninja_log, "w") as o, open(ninja_err, "w") as e: + subprocess.run( + ["ninja", "-j", str(num_procs), "install"], + cwd=build_dir, + check=True, + stdout=o, + stderr=e, + ) + except subprocess.CalledProcessError: + log.error("LLVM ninja FAILED") + return ctx.copy_with( + build_passed=False, fix_errors=[str(ninja_log), str(ninja_err)] + ) + + fc = build_dir / "bin" / "FileCheck" + if fc.exists(): + import shutil + + shutil.copy2(fc, llvm_install / "bin" / "FileCheck") + + # Regenerate patch if AI fix modified the source + if ctx.retry_count > 0: + patch_dir = ascend_path / "third_party/ascend/patch" + patch_dir.mkdir(parents=True, exist_ok=True) + new_patch_file = patch_dir / f"llvm_patch_{required_hash[:7]}.patch" + new_patch = run_git(llvm_project, "diff", "HEAD") + new_patch_file.write_text(new_patch, encoding="utf-8") + for old in patch_dir.glob("*.patch"): + if old.name != new_patch_file.name: + old.unlink() + log.info(f"Updated patch: {new_patch_file.name} ({len(new_patch)} bytes)") + + log.status(True, "LLVM build passed") + return ctx.copy_with(build_passed=True) + + +# ═══════════════════════════════════════════════════════════════════════════ +# Triton-Ascend +# ═══════════════════════════════════════════════════════════════════════════ + + +def _build_triton( + ctx: WorkflowContext, + config: TAConfig, + clean: bool = False, + python_exe: str = "python3", +) -> WorkflowContext: + """Build triton-ascend. Pure build — no retry logic.""" + ascend_path = Path(ctx.triton_ascend_path) + llvm_install = WORKSPACE_DIR / "llvm-install" + llvm_prefix = config.llvm_install_prefix or ( + str(llvm_install) if llvm_install.exists() else "" + ) + + if clean: + build_dir_path = ascend_path / "build" + if build_dir_path.exists(): + subprocess.run(["rm", "-rf", str(build_dir_path)], check=False) + + build_env = { + "LLVM_SYSPATH": llvm_prefix, + "TRITON_BUILD_WITH_CCACHE": "true", + "TRITON_BUILD_WITH_CLANG_LLD": "true", + "TRITON_BUILD_PROTON": "OFF", + "DEBUG": "1", + "TRITON_WHEEL_NAME": "triton-ascend", + "TRITON_APPEND_CMAKE_ARGS": "-DTRITON_BUILD_UT=OFF", + "MAX_JOBS": str(config.build_procs), + "CMAKE_BUILD_PARALLEL_LEVEL": str(config.build_procs), + } + + step_id = ctx.steps[ctx.current_step]["id"] if ctx.steps else "step-0" + step_dir = WORKSPACE_DIR / STEPS_DIR / step_id + step_dir.mkdir(parents=True, exist_ok=True) + build_log = step_dir / "build.log" + build_err = step_dir / "build.err" + + log.section("Build Triton-Ascend") + log.info(f"Running: {python_exe} setup.py install") + with open(build_log, "w") as o, open(build_err, "w") as e: + proc = subprocess.run( + [python_exe, "setup.py", "install"], + cwd=ascend_path, + env={**os.environ, **build_env}, + stdout=o, + stderr=e, + ) + passed = proc.returncode == 0 + + result = { + "all_passed": passed, + "steps": [ + {"step": "setup_py_install", "passed": passed, "exit_code": proc.returncode} + ], + } + (step_dir / BUILD_RESULT_FILE).write_text( + json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + if not passed: + log.error("Build FAILED") + return ctx.copy_with( + build_passed=False, fix_errors=[str(build_log), str(build_err)] + ) + log.status(True, "Build passed") + return ctx.copy_with(build_passed=True) diff --git a/src/TA_main2main_workflow/pipeline/commit.py b/src/TA_main2main_workflow/pipeline/commit.py new file mode 100644 index 0000000..514e989 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/commit.py @@ -0,0 +1,43 @@ +"""Pipeline step 7: Commit step progress.""" + +from __future__ import annotations +from pathlib import Path +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git +from TA_main2main_workflow.utils.submodule import ( + commit_submodule, + submodule_has_changes, +) +from TA_main2main_workflow.pipeline.pre_ci import cleanup_temp_files + +log = get_logger(__name__) + + +def commit_step(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + ascend_path = Path(ctx.triton_ascend_path) + step = ctx.steps[ctx.current_step] + step_id = step["id"] + + if submodule_has_changes(ascend_path): + commit_submodule( + ascend_path, f"[Sync](fix) AI fix for {ctx.target_commit[:12]}\n" + ) + + cleanup_temp_files(ascend_path) + if not run_git(ascend_path, "status", "--porcelain").strip(): + log.info(f"[{step_id}] Nothing to commit") + return ctx + + end_short = step["end_commit"][:12] + msg = f"sync: merge upstream commits for step {step_id}\n\nUpstream range: {step.get('start_commit', '?')[:12]}..{end_short}\nStep: {ctx.current_step + 1}/{ctx.total_steps}\nCommits: {step['commit_count']}\n" + try: + run_git(ascend_path, "add", "-A") + run_git(ascend_path, "commit", "-s", "-m", msg) + log.status(True, f"Committed step {step_id}") + except Exception as e: + if "nothing to commit" not in str(getattr(e, "stderr", "")): + log.warning(f"Commit failed: {e}") + + return ctx diff --git a/src/TA_main2main_workflow/pipeline/detect.py b/src/TA_main2main_workflow/pipeline/detect.py new file mode 100644 index 0000000..71441ce --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/detect.py @@ -0,0 +1,156 @@ +"""Pipeline step 1: Detect upstream commits to merge. + +Entry point: ``run_detect(ctx, config)`` — handles resume from +``detect.json`` or runs full detection from scratch. + +Calculates the commit gap between triton-ascend and upstream Triton: + - Finds merge-base + - Lists upstream commits since merge-base + - Counts changed files and lines +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils import ( + DETECT_FILE, + WORKSPACE_DIR, +) +from TA_main2main_workflow.utils.git import run_git +from TA_main2main_workflow.utils.logging import get_logger + +log = get_logger(__name__) + + +def run_detect(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Detect upstream commits (with resume support). + + If ``config.resume`` is set and ``detect.json`` exists, loads cached + results. Otherwise runs full detection via :func:`detect_commits`. + """ + detect_file = WORKSPACE_DIR / DETECT_FILE + if config.resume and detect_file.exists(): + log.info("Resume: detect.json exists, skipping detect") + data = json.loads(detect_file.read_text(encoding="utf-8")) + return ctx.copy_with( + merge_base=data["merge_base"], + target_commit=data["target_commit"], + upstream_commits=data.get("upstream_commits", []), + upstream_commits_count=data["upstream_commits_count"], + changed_files_count=data.get("changed_files_count", 0), + changed_lines_total=data.get("changed_lines", 0), + has_new_commits=True, + ascend_head=data.get("ascend_head", ""), + ) + + return _detect_commits(ctx, config) + + +def _detect_commits(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Detect upstream commits that need to be merged. + + Assumes ``prepare`` has already run (remotes configured, fetched, and + ascend_head / target_commit resolved). + + Returns updated ctx with merge_base, upstream_commits, has_new_commits, + changed_files_count, changed_lines_total. + """ + ascend_path = Path(ctx.triton_ascend_path) + ascend_head = ctx.ascend_head + target = ctx.target_commit + + # Compute merge-base + try: + merge_base = run_git(ascend_path, "merge-base", ascend_head, target).strip() + except Exception: + raise RuntimeError( + f"No common ancestor between ascend HEAD ({ascend_head[:12]}) " + f"and target ({target[:12]}).\n" + f"Ensure triton-upstream remote points to the correct triton repo:\n" + f" cd {ascend_path} && git remote -v\n" + f"Current upstream URL: {config.triton_upstream_url}" + ) + log.info(f"merge_base: {merge_base[:12]} target: {target[:12]}") + + commits = _list_upstream_commits(ascend_path, merge_base, target) + has_new = len(commits) > 0 and merge_base != target + + changed_files = _changed_files(ascend_path, merge_base, target) + changed_lines_total = _count_changed_lines(ascend_path, merge_base, target) + + result = { + "ascend_head": ascend_head, + "target_commit": target, + "merge_base": merge_base, + "upstream_commits_count": len(commits), + "upstream_commits": commits, + "changed_lines": changed_lines_total, + "changed_files": changed_files, + "changed_files_count": len(changed_files), + } + + # Write detect.json + (WORKSPACE_DIR / DETECT_FILE).write_text( + json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + return ctx.copy_with( + merge_base=merge_base, + target_commit=target, + ascend_head=ascend_head, + upstream_commits=commits, + upstream_commits_count=len(commits), + changed_files_count=result["changed_files_count"], + changed_lines_total=changed_lines_total, + has_new_commits=has_new, + ) + + +# ═══════════════════════════════════════════════════════════════════════════ +# Internal helpers (formerly in scripts/detect_commits.py) +# ═══════════════════════════════════════════════════════════════════════════ + + +def _list_upstream_commits(repo: Path, merge_base: str, target: str) -> list[dict]: + output = run_git( + repo, "log", "--reverse", "--format=%H%x1f%s", f"{merge_base}..{target}" + ) + commits: list[dict] = [] + for line in output.strip().splitlines(): + if not line.strip(): + continue + parts = line.split("\x1f", 1) + commits.append( + { + "sha": parts[0].strip(), + "subject": parts[1].strip() if len(parts) > 1 else "", + } + ) + return commits + + +def _count_changed_lines(repo: Path, merge_base: str, target: str) -> int: + """Return total lines changed between *merge_base* and *target*.""" + try: + output = run_git(repo, "diff", "--shortstat", merge_base, target) + except Exception: + return 0 + # " 97 files changed, 1234 insertions(+), 567 deletions(-)" + total = 0 + for part in output.split(","): + part = part.strip() + if "insertion" in part or "deletion" in part: + try: + total += int(part.split()[0]) + except ValueError: + pass + return total + + +def _changed_files(repo: Path, merge_base: str, target: str) -> list[str]: + output = run_git(repo, "diff", "--name-only", merge_base, target) + return sorted(f for f in output.strip().splitlines() if f) diff --git a/src/TA_main2main_workflow/pipeline/finalize.py b/src/TA_main2main_workflow/pipeline/finalize.py new file mode 100644 index 0000000..969a3e1 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/finalize.py @@ -0,0 +1,47 @@ +"""Pipeline step: Finalize — generate cumulative patch and summary.""" + +from __future__ import annotations +import time +from pathlib import Path +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git +from TA_main2main_workflow.utils.tracker import total_elapsed +from TA_main2main_workflow.utils import ( + FINAL_SUMMARY_FILE, + FINAL_TARGET_PATCH_FILE, + WORKSPACE_DIR, +) + +log = get_logger(__name__) + + +def finalize(ctx: WorkflowContext) -> WorkflowContext: + log.header("Finalize & Summary") + ascend_path = Path(ctx.triton_ascend_path) + + # Summary + summary_path = WORKSPACE_DIR / FINAL_SUMMARY_FILE + summary_path.write_text( + f"# Triton-Ascend Upstream Sync\n\n- **Target**: `{ctx.target_commit[:12]}`\n- **Steps**: {ctx.total_steps}\n- **Upstream commits**: {ctx.upstream_commits_count}\n- **Status**: Success\n- **Date**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n", + encoding="utf-8", + ) + log.info(f"Final summary: {summary_path}") + + # Patch + try: + patch = run_git(ascend_path, "diff", ctx.ascend_head, "HEAD") + (WORKSPACE_DIR / FINAL_TARGET_PATCH_FILE).write_text(patch, encoding="utf-8") + log.info(f"Cumulative patch: {len(patch)} bytes") + except Exception as e: + log.warning(f"Could not generate patch: {e}") + + log.header("Sync Complete!") + elapsed = total_elapsed() + log.elapsed(elapsed) + rows = [ + ("Finalize", "PASS", f"{ctx.total_steps} step(s)"), + ("OVERALL", "PASS", f"{ctx.total_steps} step(s)"), + ] + log.table(rows) + return ctx diff --git a/src/TA_main2main_workflow/pipeline/merge.py b/src/TA_main2main_workflow/pipeline/merge.py new file mode 100644 index 0000000..f5fc9eb --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/merge.py @@ -0,0 +1,70 @@ +"""Pipeline step: Execute git merge of upstream commits into triton-ascend.""" + +from __future__ import annotations +import json +from pathlib import Path +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git, run_git_no_check +from TA_main2main_workflow.utils import WORKSPACE_DIR, STEPS_DIR + +log = get_logger(__name__) + +_MERGE_RESULT = "merge_result.json" + + +def merge_upstream_commit(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + ascend_path = Path(ctx.triton_ascend_path) + step = ( + ctx.steps[ctx.current_step] + if ctx.steps + else {"id": "step-0", "end_commit": ctx.target_commit} + ) + step_id = step.get("id", "step-0") + step_dir = WORKSPACE_DIR / STEPS_DIR / step_id + step_dir.mkdir(parents=True, exist_ok=True) + result_file = step_dir / _MERGE_RESULT + + # Resume: skip if merge_result.json already exists for this step + if config.resume and result_file.exists(): + log.info(f"Resume: {_MERGE_RESULT} exists, skipping merge") + mr = json.loads(result_file.read_text(encoding="utf-8")) + return ctx.copy_with( + merge_has_conflicts=mr.get("has_conflicts", False), + conflict_files=mr.get("conflict_files", []), + ) + + # Abort stale merge if any + if (ascend_path / ".git" / "MERGE_HEAD").exists(): + try: + run_git(ascend_path, "merge", "--abort") + except Exception: + run_git(ascend_path, "reset", "--hard", "HEAD") + + log.info(f"Merging {step['end_commit'][:12]} ...") + merge_proc = run_git_no_check( + ascend_path, "merge", "--no-ff", "--no-edit", step["end_commit"] + ) + + conflict_files = run_git( + ascend_path, "diff", "--name-only", "--diff-filter=U" + ).strip() + conflict_files = ( + [f for f in conflict_files.splitlines() if f] if conflict_files else [] + ) + has_conflicts = len(conflict_files) > 0 + + result = { + "target_commit": step["end_commit"], + "merge_exit_code": merge_proc.returncode, + "has_conflicts": has_conflicts, + "conflict_files": conflict_files, + } + result_file.write_text( + json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + return ctx.copy_with( + merge_has_conflicts=has_conflicts, conflict_files=conflict_files + ) diff --git a/src/TA_main2main_workflow/pipeline/plan.py b/src/TA_main2main_workflow/pipeline/plan.py new file mode 100644 index 0000000..2214533 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/plan.py @@ -0,0 +1,273 @@ +"""Pipeline step: Plan steps — split upstream commits by line budget. + +Groups upstream commits into ordered steps based on changed lines in +key source directories. LLVM-hash-changing commits get solo steps. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils import ( + WORKSPACE_DIR, + STEPS_FILE, + STEPS_DIR, + LLVM_HASH_FILE, + run_git, +) +from TA_main2main_workflow.utils.logging import get_logger + +log = get_logger(__name__) + + +def run_plan(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Plan merge steps (with resume support). + + If ``config.resume`` is set and ``steps.json`` exists, loads cached + results. Otherwise runs full planning via :func:`plan_steps`. + """ + steps_file = WORKSPACE_DIR / STEPS_FILE + if config.resume and steps_file.exists(): + log.info("Resume: steps.json exists, skipping plan") + plan = json.loads(steps_file.read_text(encoding="utf-8")) + return ctx.copy_with(steps=plan["steps"], total_steps=len(plan["steps"])) + + return plan_steps(ctx, config) + + +def plan_steps(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Split upstream commits into steps and populate ctx.steps. + + If progressive_merge is disabled or there is only 1 commit, + creates a single step covering all commits. + """ + triton_path = Path(ctx.triton_ascend_path) + base = ctx.merge_base + target = ctx.target_commit + line_budget = config.line_budget + + commits = ctx.upstream_commits + log.info( + f"[plan] Scanning {len(commits)} upstream commits ({base[:8]}..{target[:8]})" + ) + log.info(f"[plan] Line budget: {line_budget}") + + if config.progressive_merge and len(commits) > 1: + lines_per_commit, llvm_commits = _scan_commits(triton_path, commits) + steps = _plan_steps_inner( + commits, lines_per_commit, base, line_budget, llvm_commits + ) + _enrich_steps(triton_path, steps) + + plan = { + "base_commit": base, + "target_commit": target, + "line_budget": line_budget, + "total_steps": len(steps), + "steps": steps, + } + _write_plan(plan) + + log.info(f"[plan] Generated {len(steps)} step(s)") + return ctx.copy_with(steps=steps, total_steps=len(steps)) + else: + return ctx.copy_with( + total_steps=1, + steps=[ + { + "index": 1, + "id": "step-1", + "commit_count": len(commits), + "start_commit": base, + "end_commit": target, + "source_changed_lines": ctx.changed_lines_total, + } + ], + ) + + +# ═══════════════════════════════════════════════════════════════════════════ +# Internal helpers (formerly in scripts/plan_steps.py) +# ═══════════════════════════════════════════════════════════════════════════ + + +def _source_lines_for_commit(repo: Path, sha: str) -> int: + """Return total lines changed in a single commit.""" + try: + output = run_git( + repo, "diff-tree", "--no-commit-id", "--shortstat", sha, quiet=True + ) + except Exception: + return 0 + # " 5 files changed, 123 insertions(+), 45 deletions(-)" + return _parse_shortstat(output) + + +def _parse_shortstat(output: str) -> int: + """Parse ``git diff --shortstat`` output and return total lines changed.""" + total = 0 + for part in output.split(","): + part = part.strip() + if "insertion" in part or "deletion" in part: + try: + total += int(part.split()[0]) + except ValueError: + pass + return total + + +def _commit_changed_llvm_hash(repo: Path, sha: str) -> bool: + try: + output = run_git( + repo, "diff-tree", "--no-commit-id", "--name-only", "-r", sha, quiet=True + ) + return LLVM_HASH_FILE in output + except Exception: + return False + + +def _scan_commits( + repo: Path, commits: list[dict[str, str]] +) -> tuple[dict[str, int], set[str]]: + lines_per_commit: dict[str, int] = {} + llvm_commits: set[str] = set() + for i, c in enumerate(commits): + lines = _source_lines_for_commit(repo, c["sha"]) + lines_per_commit[c["sha"]] = lines + if _commit_changed_llvm_hash(repo, c["sha"]): + llvm_commits.add(c["sha"]) + log.info(f"[plan] LLVM version change: {c['sha'][:8]} {c['subject'][:80]}") + if (i + 1) % 50 == 0: + log.info(f"[plan] ... scanned {i + 1}/{len(commits)} commits") + return lines_per_commit, llvm_commits + + +def _make_step( + index: int, + commits: list[dict[str, str]], + start: str, + lines: int, + budget: int, + reason: str = "line_budget", +) -> dict[str, Any]: + return { + "index": index, + "id": f"step-{index}", + "commits": commits, + "commit_count": len(commits), + "start_commit": start, + "end_commit": commits[-1]["sha"], + "source_changed_lines": lines, + "line_budget": budget, + "reason": reason, + } + + +def _plan_steps_inner( + commits: list[dict[str, str]], + lines_per_commit: dict[str, int], + base: str, + budget: int, + llvm_commits: set[str], +) -> list[dict[str, Any]]: + steps: list[dict[str, Any]] = [] + step_commits: list[dict[str, str]] = [] + step_lines = 0 + start = base + + for commit in commits: + sha = commit["sha"] + lines = lines_per_commit.get(sha, 0) + + # LLVM change → solo step + if sha in llvm_commits: + if step_commits: + steps.append( + _make_step(len(steps) + 1, step_commits, start, step_lines, budget) + ) + start = steps[-1]["end_commit"] + step_commits, step_lines = [], 0 + steps.append( + _make_step( + len(steps) + 1, + [commit], + start, + lines, + budget, + reason="llvm_version", + ) + ) + start = steps[-1]["end_commit"] + continue + + # Oversized → solo step + if lines > budget: + if step_commits: + steps.append( + _make_step(len(steps) + 1, step_commits, start, step_lines, budget) + ) + start = steps[-1]["end_commit"] + step_commits, step_lines = [], 0 + steps.append( + _make_step( + len(steps) + 1, [commit], start, lines, budget, reason="oversized" + ) + ) + start = steps[-1]["end_commit"] + continue + + # Would exceed budget → flush + if step_lines + lines > budget: + steps.append( + _make_step(len(steps) + 1, step_commits, start, step_lines, budget) + ) + start = steps[-1]["end_commit"] + step_commits, step_lines = [], 0 + + step_commits.append(commit) + step_lines += lines + + if step_commits: + steps.append( + _make_step(len(steps) + 1, step_commits, start, step_lines, budget) + ) + + return steps + + +def _enrich_steps(repo: Path, steps: list[dict[str, Any]]) -> None: + for step in steps: + step["upstream_patch"] = run_git( + repo, "diff", f"{step['start_commit']}..{step['end_commit']}", quiet=True + ) + step["changed_files"] = run_git( + repo, + "diff", + "--name-only", + f"{step['start_commit']}..{step['end_commit']}", + quiet=True, + ) + + +def _write_plan(plan: dict[str, Any]) -> None: + steps_dir = WORKSPACE_DIR / STEPS_DIR + steps_dir.mkdir(parents=True, exist_ok=True) + (WORKSPACE_DIR / STEPS_FILE).write_text( + json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + for step in plan["steps"]: + step_dir = steps_dir / step["id"] + step_dir.mkdir(parents=True, exist_ok=True) + (step_dir / "upstream.patch").write_text( + step["upstream_patch"], encoding="utf-8" + ) + (step_dir / "changed_files.txt").write_text( + step["changed_files"], encoding="utf-8" + ) + lines = [f"{c['sha'][:8]} {c['subject']}" for c in step["commits"]] + (step_dir / "commits.txt").write_text("\n".join(lines) + "\n", encoding="utf-8") diff --git a/src/TA_main2main_workflow/pipeline/pre_ci.py b/src/TA_main2main_workflow/pipeline/pre_ci.py new file mode 100644 index 0000000..96bf616 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/pre_ci.py @@ -0,0 +1,145 @@ +"""Pre-CI verification: conflict markers, Python syntax, temp file cleanup.""" + +from __future__ import annotations +import ast, json, subprocess +from pathlib import Path +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git_no_check +from TA_main2main_workflow.utils import PRE_CI_CHECK_FILE, WORKSPACE_DIR + +log = get_logger(__name__) + +_CLEANUP_DIRS = [ + "result_profiling", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + "*.egg-info", +] +_CLEANUP_SUFFIXES = [".lock", ".pyc", ".pyo", ".orig", ".rej", ".log"] +_CLEANUP_BASENAMES = [".DS_Store"] +_CONFLICT_MARKERS = ["<<<<<<<", "=======", ">>>>>>>"] + + +def cleanup_temp_files(repo: Path): + import shutil + + removed_dirs, removed_files = [], [] + for d in _CLEANUP_DIRS: + for found in repo.rglob(d): + if found.is_dir() and ".git" not in found.parts: + try: + shutil.rmtree(found, ignore_errors=True) + removed_dirs.append(str(found.relative_to(repo))) + except Exception: + pass + for suffix in _CLEANUP_SUFFIXES: + for found in repo.rglob(f"*{suffix}"): + if found.is_file() and ".git" not in found.parts: + try: + found.unlink() + removed_files.append(str(found.relative_to(repo))) + except Exception: + pass + for name in _CLEANUP_BASENAMES: + for found in repo.rglob(name): + if found.is_file() and ".git" not in found.parts: + try: + found.unlink() + removed_files.append(str(found.relative_to(repo))) + except Exception: + pass + total = len(removed_dirs) + len(removed_files) + if total > 0: + log.info(f"Cleaned up {total} temp artifact(s)") + + +def _get_modified_files(repo: Path) -> list[str]: + modified: set[str] = set() + for args in [ + ("diff", "--name-only", "HEAD"), + ("diff", "--name-only", "--cached"), + ("ls-files", "--others", "--exclude-standard"), + ]: + r = run_git_no_check(repo, *args) + if r.stdout.strip(): + modified.update(r.stdout.strip().splitlines()) + return sorted(modified) + + +def _check_conflict_markers(repo: Path, files: list[str]) -> dict: + violations = [] + for fp in files: + full = repo / fp + if not full.is_file(): + continue + try: + content = full.read_text(encoding="utf-8", errors="replace") + except Exception: + continue + for lineno, line in enumerate(content.splitlines(), 1): + for m in _CONFLICT_MARKERS: + if line.strip().startswith(m): + violations.append({"file": fp, "line": lineno, "marker": m}) + return { + "name": "conflict_markers", + "passed": len(violations) == 0, + "violations": violations, + } + + +def _check_python_syntax(repo: Path, files: list[str]) -> dict: + violations = [] + for fp in [f for f in files if f.endswith(".py")]: + full = repo / fp + if not full.is_file(): + continue + try: + ast.parse(full.read_text(encoding="utf-8"), filename=fp) + except SyntaxError as e: + violations.append({"file": fp, "line": e.lineno or 0, "msg": str(e.msg)}) + except Exception: + pass + return { + "name": "python_syntax", + "passed": len(violations) == 0, + "violations": violations, + } + + +def run_pre_ci_check(repo: Path, step_id: str = "") -> dict: + log.section(f"Pre-CI Check{' — ' + step_id if step_id else ''}") + try: + modified_files = _get_modified_files(repo) + except Exception as e: + log.warning(f"Could not list modified files: {e}") + return {"all_passed": True, "checks": []} + if not modified_files: + log.info("No modified files") + return {"all_passed": True, "checks": [], "modified_files_count": 0} + + cleanup_temp_files(repo) + try: + modified_files = _get_modified_files(repo) + except Exception: + pass + + all_passed = True + conflict = _check_conflict_markers(repo, modified_files) + log.status(conflict["passed"], conflict.get("detail", "")) + if not conflict["passed"]: + all_passed = False + syntax = _check_python_syntax(repo, modified_files) + log.status(syntax["passed"], syntax.get("detail", "")) + if not syntax["passed"]: + all_passed = False + + result = { + "all_passed": all_passed, + "checks": [conflict, syntax], + "modified_files_count": len(modified_files), + } + (WORKSPACE_DIR / PRE_CI_CHECK_FILE).write_text( + json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + return result diff --git a/src/TA_main2main_workflow/pipeline/prepare.py b/src/TA_main2main_workflow/pipeline/prepare.py new file mode 100644 index 0000000..759abd1 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/prepare.py @@ -0,0 +1,155 @@ +"""Pipeline step 0: Prepare workspace — clone repos, configure remotes, fetch. + +This is the first step of the workflow. It ensures the local environment +is ready before any detection or merge work begins: + +1. Clone triton-ascend if no local path is given (skip if already exists) +2. Verify ``origin`` points to the correct remote URL; fix if not +3. Ensure ``triton-upstream`` remote exists, pointing to upstream Triton +4. Fetch both remotes (with built-in retry) +5. Checkout the configured base branch, fast-forward to origin + +Output context fields set: + - ``origin_remote``, ``upstream_remote`` — remote names + - ``triton_ascend_path`` — absolute path to the triton-ascend repo + - ``target_commit`` — the upstream commit to sync to (HEAD of + triton-upstream/main when not explicitly given) + - ``ascend_head`` — the HEAD of the configured base branch + - ``original_branch`` — the base branch name +""" + +from __future__ import annotations + +from pathlib import Path + +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git, run_git_no_check +from TA_main2main_workflow.utils import WORKSPACE_DIR + +log = get_logger(__name__) + +ORIGIN_REMOTE = "origin" +UPSTREAM_REMOTE = "triton-upstream" + + +def prepare(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Set up workspace: clone, remotes, fetch, checkout. + + This is idempotent — safe to call on an already-prepared workspace. + """ + WORKSPACE_DIR.mkdir(parents=True, exist_ok=True) + + # ── 1. Ensure triton-ascend exists ────────────────────────────────── + ascend_path = _ensure_repo(config, WORKSPACE_DIR) + + # ── 2. Ensure origin points to the correct remote URL ─────────────── + _fix_origin(ascend_path, config.triton_ascend_url) + + # ── 3. Ensure triton-upstream remote ──────────────────────────────── + _ensure_remote(ascend_path, UPSTREAM_REMOTE, config.triton_upstream_url) + + # ── 4. Fetch both remotes ─────────────────────────────────────────── + log.info(f"Fetching {ORIGIN_REMOTE} ...") + run_git(ascend_path, "fetch", ORIGIN_REMOTE) + log.info(f"Fetching {UPSTREAM_REMOTE} ...") + run_git(ascend_path, "fetch", UPSTREAM_REMOTE) + + # ── 5. Checkout base branch ───────────────────────────────────────── + base_branch = config.base_branch + base_ref = f"{ORIGIN_REMOTE}/{base_branch}" + + # Force checkout to origin's version of the base branch + run_git(ascend_path, "checkout", "-B", base_branch, base_ref) + + # ── 6. Resolve ascend HEAD ────────────────────────────────────────── + try: + ascend_head = run_git(ascend_path, "rev-parse", base_ref).strip() + except Exception: + raise RuntimeError( + f"Cannot resolve '{base_ref}'. " + f"Fetch it first:\n" + f" cd {ascend_path} && git fetch {ORIGIN_REMOTE} {base_branch}" + ) + + # ── 7. Resolve target commit (default: triton-upstream/main HEAD) ── + target_commit = config.target_commit + if not target_commit: + upstream_ref = f"{UPSTREAM_REMOTE}/main" + try: + target_commit = run_git(ascend_path, "rev-parse", upstream_ref).strip() + except Exception: + raise RuntimeError( + f"Cannot resolve upstream HEAD from '{upstream_ref}'. " + f"Specify --target-commit or ensure '{upstream_ref}' exists. " + f"Try: cd {ascend_path} && git fetch {UPSTREAM_REMOTE}" + ) + + log.section("Workspace ready") + log.key_value("triton-ascend", str(ascend_path)) + log.key_value("base branch", base_branch) + log.key_value("ascend HEAD", ascend_head[:12]) + log.key_value("target commit", target_commit[:12]) + + return ctx.copy_with( + triton_ascend_path=str(ascend_path), + target_commit=target_commit, + ascend_head=ascend_head, + origin_remote=ORIGIN_REMOTE, + upstream_remote=UPSTREAM_REMOTE, + ) + + +# ═══════════════════════════════════════════════════════════════════════════ +# Internal helpers +# ═══════════════════════════════════════════════════════════════════════════ + + +def _ensure_repo(config: TAConfig, workspace: Path) -> Path: + """Return path to triton-ascend repo, cloning if necessary.""" + if config.triton_ascend_path: + path = Path(config.triton_ascend_path) + if not path.exists(): + raise FileNotFoundError(f"triton-ascend path does not exist: {path}") + log.info(f"Using existing repo: {path}") + return path + + target = workspace / "triton-ascend" + if target.exists(): + log.info(f"Repo exists, skip clone: {target}") + else: + log.info(f"Cloning {config.triton_ascend_url} → {target}") + run_git(workspace, "clone", config.triton_ascend_url, str(target)) + return target + + +def _ensure_remote(repo: Path, name: str, url: str) -> None: + """Add a git remote if it doesn't already exist.""" + result = run_git_no_check(repo, "remote") + if name not in result.stdout: + run_git(repo, "remote", "add", name, url) + + +def _fix_origin(repo: Path, expected_url: str) -> None: + """Ensure ``origin`` points to *expected_url*. Update it if not.""" + current = _get_remote_url(repo, ORIGIN_REMOTE) + if current is None: + log.warning(f"No '{ORIGIN_REMOTE}' remote found — adding it") + run_git(repo, "remote", "add", ORIGIN_REMOTE, expected_url) + return + + if current.rstrip("/") == expected_url.rstrip("/"): + log.info(f"origin URL OK: {current}") + return + + log.warning(f"origin URL mismatch — updating to {expected_url}") + run_git(repo, "remote", "set-url", ORIGIN_REMOTE, expected_url) + + +def _get_remote_url(repo: Path, name: str) -> str | None: + """Return the fetch URL of remote *name*, or None if it doesn't exist.""" + result = run_git_no_check(repo, "remote", "get-url", name) + if result.returncode == 0: + return result.stdout.strip() + return None diff --git a/src/TA_main2main_workflow/scripts/push_to_github.py b/src/TA_main2main_workflow/pipeline/push_pr.py similarity index 64% rename from src/TA_main2main_workflow/scripts/push_to_github.py rename to src/TA_main2main_workflow/pipeline/push_pr.py index b64038d..90f215d 100644 --- a/src/TA_main2main_workflow/scripts/push_to_github.py +++ b/src/TA_main2main_workflow/pipeline/push_pr.py @@ -29,10 +29,17 @@ from pathlib import Path from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, FINAL_TARGET_PATCH_FILE, FINAL_SUMMARY_FILE, - run_git, run_git_no_check, print_error, - ENV_BASE_BRANCH, get_base_branch_ref, + WORKSPACE_DIR, + FINAL_TARGET_PATCH_FILE, + FINAL_SUMMARY_FILE, + run_git, + run_git_no_check, + ENV_BASE_BRANCH, + get_base_branch_ref, ) +from TA_main2main_workflow.utils.logging import get_logger + +log = get_logger(__name__) def _detect_origin_owner(repo: Path, remote: str = "origin") -> str: @@ -93,46 +100,52 @@ def _ensure_gh_auth(repo: Path) -> None: try: subprocess.run( ["gh", "auth", "status"], - check=True, capture_output=True, text=True, + check=True, + capture_output=True, + text=True, ) - print("[push] gh CLI already authenticated.") + log.info("[push] gh CLI already authenticated.") except subprocess.CalledProcessError: - print( + log.error( "[push] gh not authenticated and GH_TOKEN not set. " - "Run 'gh auth login' locally or set GH_TOKEN in CI.", - file=sys.stderr, + "Run 'gh auth login' locally or set GH_TOKEN in CI." ) sys.exit(1) subprocess.run( ["gh", "auth", "setup-git"], - check=True, capture_output=True, text=True, + check=True, + capture_output=True, + text=True, ) - print("[push] Git credential helper configured (via gh auth setup-git).") + log.info("[push] Git credential helper configured (via gh auth setup-git).") return # ── GH_TOKEN is set ── - print("[push] Using GH_TOKEN from environment") + log.info("[push] Using GH_TOKEN from environment") # Step 1: Explicitly login gh CLI against github.com. # This is essential when the git remote points to a proxy host — # gh needs to know about github.com independently of git remotes. result = subprocess.run( ["gh", "auth", "login", "--with-token", "--hostname", "github.com"], - input=gh_token + "\n", text=True, capture_output=True, + input=gh_token + "\n", + text=True, + capture_output=True, ) if result.returncode == 0: - print("[push] gh auth login --with-token: success") + log.info("[push] gh auth login --with-token: success") else: - print(f"[push] gh auth login stderr: {result.stderr.strip()}") + log.info(f"[push] gh auth login stderr: {result.stderr.strip()}") # Step 2: Verify the token works result = subprocess.run( ["gh", "auth", "status", "--hostname", "github.com"], - capture_output=True, text=True, + capture_output=True, + text=True, ) - print(f"[push] gh auth status: {result.stdout.strip()}") + log.info(f"[push] gh auth status: {result.stdout.strip()}") if result.returncode != 0: - print(f"[push] gh auth status stderr: {result.stderr.strip()}") + log.info(f"[push] gh auth status stderr: {result.stderr.strip()}") # Step 3: Configure git credential helper (best-effort). # This may fail when the git remote points to a proxy host that gh @@ -140,13 +153,16 @@ def _ensure_gh_auth(repo: Path) -> None: # the token directly in the origin URL. result = subprocess.run( ["gh", "auth", "setup-git", "--hostname", "github.com"], - capture_output=True, text=True, + capture_output=True, + text=True, ) if result.returncode == 0: - print("[push] Git credential helper configured (via gh auth setup-git).") + log.info("[push] Git credential helper configured (via gh auth setup-git).") else: - print(f"[push] gh auth setup-git skipped " - f"(exit {result.returncode}): {result.stderr.strip()}") + log.info( + f"[push] gh auth setup-git skipped " + f"(exit {result.returncode}): {result.stderr.strip()}" + ) # Step 4: Embed token in origin URL so git push works through the proxy. # (gh auth setup-git may not help when url.insteadOf rewrites the host.) @@ -159,9 +175,9 @@ def _ensure_gh_auth(repo: Path) -> None: new_url = f"https://x-access-token:{gh_token}@{clean_url}" run_git(repo, "remote", "set-url", "origin", new_url) safe = f"https://x-access-token:***@{clean_url}" - print(f"[push] origin URL rewritten with token: {safe}") + log.info(f"[push] origin URL rewritten with token: {safe}") except Exception as exc: - print(f"[push] Note: could not rewrite origin URL: {exc}") + log.info(f"[push] Note: could not rewrite origin URL: {exc}") def _run_pre_commit_and_amend(repo: Path) -> bool: @@ -176,18 +192,18 @@ def _run_pre_commit_and_amend(repo: Path) -> bool: Returns True if pre-commit passed (with or without auto-fixes). Returns False if pre-commit found unfixable issues. """ - from TA_main2main_workflow.scripts.pre_ci_check import cleanup_temp_files + from TA_main2main_workflow.pipeline.pre_ci import cleanup_temp_files base_ref = get_base_branch_ref() - print("[push] ── Pre-commit check before PR ──") + log.info("[push] ── Pre-commit check before PR ──") # ── Step 1: clean temp files ── - print("[push] Cleaning temp files before pre-commit...") + log.info("[push] Cleaning temp files before pre-commit...") cleanup_temp_files(repo) # ── Step 2: run pre-commit ── - print(f"[push] Running: pre-commit run --from-ref {base_ref} --to-ref HEAD") + log.info(f"[push] Running: pre-commit run --from-ref {base_ref} --to-ref HEAD") try: pc_proc = subprocess.run( ["pre-commit", "run", "--from-ref", base_ref, "--to-ref", "HEAD"], @@ -197,17 +213,17 @@ def _run_pre_commit_and_amend(repo: Path) -> bool: timeout=300, ) except subprocess.TimeoutExpired: - print("[push] ⚠ pre-commit timed out after 300s, continuing anyway") + log.info("[push] ⚠ pre-commit timed out after 300s, continuing anyway") return True except FileNotFoundError: - print("[push] ⚠ pre-commit not installed, skipping") + log.info("[push] ⚠ pre-commit not installed, skipping") return True # Print pre-commit output if pc_proc.stdout: - print(pc_proc.stdout) + log.info(pc_proc.stdout) if pc_proc.stderr: - print(pc_proc.stderr, file=sys.stderr) + log.error(pc_proc.stderr) precommit_passed = pc_proc.returncode == 0 @@ -216,23 +232,25 @@ def _run_pre_commit_and_amend(repo: Path) -> bool: has_modifications = bool(status_proc.stdout.strip()) if has_modifications: - print("[push] Pre-commit modified files, amending latest commit...") + log.info("[push] Pre-commit modified files, amending latest commit...") # Stage only tracked files to avoid temp artifacts run_git(repo, "add", "-u") try: run_git(repo, "commit", "--amend", "--no-edit") - print("[push] Commit amended with pre-commit fixes.") + log.info("[push] Commit amended with pre-commit fixes.") except subprocess.CalledProcessError: - print("[push] Nothing to amend (already clean)") + log.info("[push] Nothing to amend (already clean)") # ── Step 4: re-clean temp files after amend ── cleanup_temp_files(repo) else: if precommit_passed: - print("[push] Pre-commit passed, no modifications needed.") + log.info("[push] Pre-commit passed, no modifications needed.") else: - print("[push] ⚠ Pre-commit reported issues but no files were modified " - "(may need manual review).") + log.info( + "[push] ⚠ Pre-commit reported issues but no files were modified " + "(may need manual review)." + ) return True @@ -249,7 +267,9 @@ def _build_pr_title(target_commit: str = "") -> str: author = os.getenv("PR_AUTHOR", "Sync").strip() pr_type = os.getenv("PR_TYPE", "feat").strip() if target_commit: - return f"[{author}]({pr_type}) Merge upstream triton commits {target_commit[:8]}" + return ( + f"[{author}]({pr_type}) Merge upstream triton commits {target_commit[:8]}" + ) ts = datetime.now().strftime("%Y%m%d-%H%M%S") return f"[{author}]({pr_type}) Merge upstream triton commits {ts}" @@ -269,12 +289,14 @@ def _create_pr_via_api( url.insteadOf proxy. """ url = f"https://api.github.com/repos/{github_repo}/pulls" - payload = json.dumps({ - "title": title, - "body": body, - "head": head, - "base": base, - }).encode("utf-8") + payload = json.dumps( + { + "title": title, + "body": body, + "head": head, + "base": base, + } + ).encode("utf-8") req = urllib.request.Request( url, @@ -297,9 +319,7 @@ def _create_pr_via_api( return pr_url except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8", errors="replace") - raise RuntimeError( - f"GitHub API error {e.code}: {error_body}" - ) from e + raise RuntimeError(f"GitHub API error {e.code}: {error_body}") from e def _create_pr_via_gh( @@ -318,25 +338,31 @@ def _create_pr_via_gh( """ gh_token = os.environ.get("GH_TOKEN") or "" gh_cmd = [ - "gh", "pr", "create", - "--title", title, - "--body", body, - "--head", head_ref, - "--base", base_branch, - "--repo", github_repo, + "gh", + "pr", + "create", + "--title", + title, + "--body", + body, + "--head", + head_ref, + "--base", + base_branch, + "--repo", + github_repo, ] - print(f"[push] Running: GH_HOST=github.com {' '.join(gh_cmd)}") + log.info(f"[push] Running: GH_HOST=github.com {' '.join(gh_cmd)}") result = subprocess.run( gh_cmd, - capture_output=True, text=True, timeout=60, - env={**os.environ, - "GITHUB_TOKEN": gh_token, - "GH_TOKEN": gh_token}, + capture_output=True, + text=True, + timeout=60, + env={**os.environ, "GITHUB_TOKEN": gh_token, "GH_TOKEN": gh_token}, ) if result.returncode != 0: raise RuntimeError( - f"gh pr create failed (exit {result.returncode}): " - f"{result.stderr.strip()}" + f"gh pr create failed (exit {result.returncode}): {result.stderr.strip()}" ) pr_url = result.stdout.strip() if not pr_url: @@ -382,7 +408,7 @@ def push_and_create_pr( patch_content = run_git(repo, "diff", merge_base, "HEAD") patch_path = WORKSPACE_DIR / FINAL_TARGET_PATCH_FILE patch_path.write_text(patch_content, encoding="utf-8") - print(f"[push] Cumulative patch written to {patch_path}") + log.info(f"[push] Cumulative patch written to {patch_path}") summary_file = summary_path or (WORKSPACE_DIR / FINAL_SUMMARY_FILE) if not summary_file.exists(): @@ -401,36 +427,42 @@ def push_and_create_pr( # ── Commit any remaining uncommitted changes (after pre-commit amend) ── status = run_git(repo, "status", "--porcelain").strip() if status: - print("[push] Staging uncommitted changes...") + log.info("[push] Staging uncommitted changes...") # Use "git add -u" (tracked-only) to avoid staging test artifacts, # cache files, or other transient files created during the flow. run_git(repo, "add", "-u") - commit_msg = f"sync: upstream triton merge ({datetime.now().strftime('%Y%m%d-%H%M%S')})" + commit_msg = ( + f"sync: upstream triton merge ({datetime.now().strftime('%Y%m%d-%H%M%S')})" + ) try: run_git(repo, "commit", "-s", "-m", commit_msg) - print(f"[push] Committed: {commit_msg}") + log.info(f"[push] Committed: {commit_msg}") except subprocess.CalledProcessError: - print("[push] Nothing to commit (already clean)") + log.info("[push] Nothing to commit (already clean)") # ── Push ── - print(f"[push] Pushing branch '{work_branch}' to origin...") + log.info(f"[push] Pushing branch '{work_branch}' to origin...") # Debug: show what token / URL we're actually using - print("[push] === DEBUG push environment ===") - print(f"[push] GH_TOKEN set: {bool(os.getenv('GH_TOKEN'))}") - print(f"[push] GITHUB_TOKEN set: {bool(os.getenv('GITHUB_TOKEN'))}") + log.info("[push] === DEBUG push environment ===") + log.info(f"[push] GH_TOKEN set: {bool(os.getenv('GH_TOKEN'))}") + log.info(f"[push] GITHUB_TOKEN set: {bool(os.getenv('GITHUB_TOKEN'))}") try: remote_url = run_git(repo, "remote", "get-url", "origin").strip() # Mask any embedded token if "@" in remote_url: - safe_url = remote_url.split("@")[0].split(":")[-1] + "@" + remote_url.split("@")[1] + safe_url = ( + remote_url.split("@")[0].split(":")[-1] + "@" + remote_url.split("@")[1] + ) else: safe_url = remote_url - print(f"[push] origin URL: {safe_url}") - print(f"[push] current branch: {run_git(repo, 'branch', '--show-current').strip()}") + log.info(f"[push] origin URL: {safe_url}") + log.info( + f"[push] current branch: {run_git(repo, 'branch', '--show-current').strip()}" + ) except Exception: pass - print("[push] ==============================") + log.info("[push] ==============================") # Push to the fork (same pattern as AscendNPU-IR submodule push). # Token embedded in the URL so the CI proxy can authenticate. @@ -447,26 +479,33 @@ def push_and_create_pr( run_git_no_check(repo, "remote", "remove", _fork_remote) run_git(repo, "remote", "add", _fork_remote, _fork_url) _push_result = subprocess.run( - ["git", - "-c", "http.https://github.com/.extraheader=", - "push", "--force-with-lease", _fork_remote, work_branch], - cwd=str(repo), capture_output=True, text=True, + [ + "git", + "-c", + "http.https://github.com/.extraheader=", + "push", + "--force-with-lease", + _fork_remote, + work_branch, + ], + cwd=str(repo), + capture_output=True, + text=True, ) run_git(repo, "remote", "remove", _fork_remote) if _push_result.returncode == 0: if _push_result.stdout.strip(): - print(f"[push] stdout:\n{_push_result.stdout.strip()}") + log.info(f"[push] stdout:\n{_push_result.stdout.strip()}") break _last_push_error = _push_result.stderr.strip() or "(no stderr)" - print_error( + log.error( f"[push] git push attempt {_attempt}/5 FAILED " f"(exit {_push_result.returncode}):\n{_last_push_error}" ) if _attempt < 5: time.sleep(10 * _attempt) else: - raise RuntimeError( - f"git push failed after 5 attempts: {_last_push_error}") + raise RuntimeError(f"git push failed after 5 attempts: {_last_push_error}") else: run_git(repo, "push", "-u", "origin", work_branch) @@ -475,18 +514,24 @@ def push_and_create_pr( # to the proxy, so we temporarily swap it to the fork URL (with # token) — gh recognizes github.com and GH_HOST isn't needed. base_branch = os.getenv("TA_PR_BASE_BRANCH", "upstream-sync") - pr_description = summary_file.read_text(encoding="utf-8") if summary_file.exists() else "" + pr_description = ( + summary_file.read_text(encoding="utf-8") if summary_file.exists() else "" + ) _head = f"{_fork_owner}:{work_branch}" if _fork_owner else work_branch pr_title = _build_pr_title(target_commit) - print(f"[push] Creating PR via gh CLI:") - print(f" head = {_head}") - print(f" base = {base_branch}") - print(f" repo = {github_repo}") + log.info(f"[push] Creating PR via gh CLI:") + log.info(f" head = {_head}") + log.info(f" base = {base_branch}") + log.info(f" repo = {github_repo}") _saved_origin = run_git(repo, "config", "--get", "remote.origin.url").strip() - _pr_origin = f"https://x-access-token:{_token}@github.com/{_fork_owner}/triton-ascend.git" if _token else f"https://github.com/{_fork_owner}/triton-ascend.git" + _pr_origin = ( + f"https://x-access-token:{_token}@github.com/{_fork_owner}/triton-ascend.git" + if _token + else f"https://github.com/{_fork_owner}/triton-ascend.git" + ) run_git(repo, "remote", "set-url", "origin", _pr_origin) _last_pr_error = "" @@ -499,114 +544,13 @@ def push_and_create_pr( head_ref=_head, base_branch=base_branch, ) - print(f"[push] PR created: {pr_url}") + log.info(f"[push] PR created: {pr_url}") return pr_url except Exception as _e: _last_pr_error = str(_e) - print_error(f"[push] PR create attempt {_attempt}/5 FAILED: " - f"{_last_pr_error}") + log.error(f"[push] PR create attempt {_attempt}/5 FAILED: {_last_pr_error}") if _attempt < 5: time.sleep(10 * _attempt) finally: run_git(repo, "remote", "set-url", "origin", _saved_origin) - raise RuntimeError( - f"gh pr create failed after 5 attempts: {_last_pr_error}") - - -def push_step_progress( - ascend_path: Path, - github_repo: str = "triton-lang/triton-ascend", - work_branch: str = "", - step_id: str = "", - step_num: int = 1, - total_steps: int = 1, - pr_url: str = "", -) -> str: - """Push work-branch progress after a single step and create/update a PR. - - Called after each progressive step's commit. On the first call (pr_url - is empty) it creates a new PR; on subsequent calls it just pushes — - the existing PR picks up the new commits automatically. - - Returns the PR URL (new or existing). - """ - repo = Path(ascend_path) - - if not work_branch: - work_branch = run_git(repo, "branch", "--show-current").strip() - - _ensure_gh_auth(repo) - - # ── Generate step-aware patch ── - patch_content = run_git(repo, "diff", get_base_branch_ref(), "HEAD") - patch_path = WORKSPACE_DIR / FINAL_TARGET_PATCH_FILE - patch_path.write_text(patch_content, encoding="utf-8") - - # ── Push ── - print(f"[push] [{step_id}] Pushing branch '{work_branch}' to origin...") - run_git(repo, "push", "-u", "origin", work_branch) - - # ── Create PR on first call only ── - if not pr_url: - base_branch = _detect_default_branch(repo) - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - pr_title = ( - f"[Step {step_num}/{total_steps}] sync: upstream triton merge ({ts})" - ) - pr_body = ( - f"## Progressive Sync — Step {step_num}/{total_steps}\n\n" - f"**Work branch**: `{work_branch}`\n" - f"**Target repo**: `{github_repo}`\n" - f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" - f"This PR will be updated as subsequent steps complete.\n" - ) - - print(f"[push] [{step_id}] Creating PR: {pr_title}") - gh_cmd = [ - "gh", "pr", "create", - "--title", pr_title, - "--body", pr_body, - "--head", work_branch, - "--base", base_branch, - "--repo", github_repo, - ] - result = subprocess.run( - gh_cmd, check=True, capture_output=True, text=True, cwd=str(repo) - ) - pr_url = result.stdout.strip() - print(f"[push] [{step_id}] PR created: {pr_url}") - else: - print(f"[push] [{step_id}] Pushed to existing PR: {pr_url}") - - return pr_url - - -def update_pr_description( - ascend_path: Path, - github_repo: str, - pr_url: str, - step_descriptions: list[str], -) -> None: - """Update the PR body with a summary of all completed steps.""" - if not pr_url: - return - - body = ( - "# Triton-Ascend Progressive Upstream Sync\n\n" - "## Completed Steps\n\n" - ) - for desc in step_descriptions: - body += f"- {desc}\n" - body += ( - f"\n---\n" - f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" - ) - - try: - subprocess.run( - ["gh", "pr", "edit", pr_url, "--body", body, "--repo", github_repo], - check=True, capture_output=True, text=True, cwd=str(ascend_path), - ) - print(f"[push] Updated PR description: {pr_url}") - except subprocess.CalledProcessError as e: - print(f"[push] Warning: could not update PR description: {e}") + raise RuntimeError(f"gh pr create failed after 5 attempts: {_last_pr_error}") diff --git a/src/TA_main2main_workflow/pipeline/resolve.py b/src/TA_main2main_workflow/pipeline/resolve.py new file mode 100644 index 0000000..661ba7c --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/resolve.py @@ -0,0 +1,84 @@ +"""Pipeline step 5: AI resolve merge conflicts.""" + +from __future__ import annotations +import json +from pathlib import Path +from TA_main2main_workflow.pipeline.ai_fix import run_opencode_adapter +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git +from TA_main2main_workflow.pipeline.pre_ci import cleanup_temp_files, run_pre_ci_check +from TA_main2main_workflow.utils import STEPS_DIR, WORKSPACE_DIR + +log = get_logger(__name__) +_REF = str(Path(__file__).parent.parent / "reference") + + +def resolve_conflicts(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + if config.skip_ai_analysis: + log.warning("SKIP_AI_ANALYSIS=true — cannot resolve conflicts") + return ctx + ascend_path = Path(ctx.triton_ascend_path) + step = ctx.steps[ctx.current_step] if ctx.current_step < len(ctx.steps) else None + step_id = step["id"] if step else "step-0" + step_dir = WORKSPACE_DIR / STEPS_DIR / step_id + step_dir.mkdir(parents=True, exist_ok=True) + + log.header("AI Conflict Resolution") + for attempt in range(1, config.max_retries + 1): + log.step(attempt, config.max_retries, "AI conflict resolution") + cf = [ + f + for f in run_git(ascend_path, "diff", "--name-only", "--diff-filter=U") + .strip() + .splitlines() + if f + ] + if not cf: + log.status(True, "Already resolved!") + break + try: + run_opencode_adapter( + { + "step_id": f"{step_id}-conflict-{attempt}", + "step_dir": str(step_dir), + "conflict_dir": str(step_dir), + "ascend_path": str(ascend_path), + "triton_path": ctx.triton_ascend_path, + "reference_dir": _REF, + "mode": "conflict", + "error_logs": json.dumps(cf, ensure_ascii=False), + "target_commit": ctx.target_commit, + "step_index": f"{ctx.current_step + 1}/{ctx.total_steps}", + } + ) + except Exception as e: + log.error(f"AI call failed: {e}") + if attempt < config.max_retries: + continue + break + if not run_git(ascend_path, "diff", "--name-only", "--diff-filter=U").strip(): + log.status(True, f"Resolved (attempt {attempt})") + break + cf_remain = [ + f + for f in run_git(ascend_path, "diff", "--name-only", "--diff-filter=U") + .strip() + .splitlines() + if f + ] + log.status(False, f"{len(cf_remain)} conflict(s) remain") + else: + log.error(f"Failed after {config.max_retries} attempts") + return ctx + + cleanup_temp_files(ascend_path) + try: + run_git(ascend_path, "add", "-A") + run_git(ascend_path, "commit", "--no-edit", "-s") + log.status(True, "Committed resolution") + except Exception: + pass + run_pre_ci_check(ascend_path, step_id="conflict-resolution") + return ctx.copy_with(merge_has_conflicts=False) diff --git a/src/TA_main2main_workflow/pipeline/test.py b/src/TA_main2main_workflow/pipeline/test.py new file mode 100644 index 0000000..9ac12d9 --- /dev/null +++ b/src/TA_main2main_workflow/pipeline/test.py @@ -0,0 +1,105 @@ +"""Pipeline step: Run pytest unit tests on Ascend NPU with retry/fix loop.""" + +from __future__ import annotations +import json, os, shutil, subprocess, time, xml.etree.ElementTree as ET +from pathlib import Path +from TA_main2main_workflow.utils.config import TAConfig +from TA_main2main_workflow.utils.context import WorkflowContext +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.tracker import timed +from TA_main2main_workflow.utils import TEST_RESULT_FILE, WORKSPACE_DIR +from TA_main2main_workflow.pipeline.ai_fix import ai_fix + +log = get_logger(__name__) + + +def test(ctx: WorkflowContext, config: TAConfig) -> WorkflowContext: + """Test phase: run pytest with retry+fix loop.""" + if config.skip_e2e_test: + log.info("SKIP_E2E_TEST=true — treating tests as passed") + return ctx.copy_with(test_passed=True) + + for attempt in range(config.max_retries + 1): + ctx = ctx.copy_with(retry_count=attempt) + if attempt > 0: + log.header(f"Test Fix Attempt {attempt}/{config.max_retries}") + ctx = ai_fix(ctx, config, attempt=attempt, mode="test_fix") + with timed("test"): + ctx = _run_pytest(ctx, config) + if ctx.test_passed: + return ctx + log.info(f"Tests failed (attempt {attempt + 1}) — retrying") + return ctx.copy_with(test_passed=False) + + +def _run_pytest( + ctx: WorkflowContext, config: TAConfig, python_exe: str = "" +) -> WorkflowContext: + """Execute pytest and return updated ctx with test_passed + fix_errors.""" + ascend_path = Path(ctx.triton_ascend_path) + test_log_dir = WORKSPACE_DIR / "test-logs" + test_log_dir.mkdir(parents=True, exist_ok=True) + + test_dir_path = (ascend_path / "third_party/ascend/unittest/pytest_ut").resolve() + python_exe = python_exe or os.getenv("PYTHON", "python3.10") + + if not test_dir_path.exists(): + log.warning(f"Test directory not found: {test_dir_path}") + return ctx.copy_with(test_passed=True) + + junit_xml = test_log_dir / "pytest-junit.xml" + pytest_bin = shutil.which("pytest") + cmd = ( + [pytest_bin, str(test_dir_path)] + if pytest_bin + else [python_exe, "-m", "pytest", str(test_dir_path)] + ) + cmd += ["-n", str(config.test_procs), f"--junitxml={junit_xml}"] + + log.section("Run Tests") + log.info(f"cmd: {' '.join(cmd)}") + _start = time.time() + try: + result = subprocess.run(cmd, cwd=ascend_path, timeout=1000) + rc = result.returncode + except subprocess.TimeoutExpired: + rc = -1 + log.warning("pytest timed out after 1000s") + + elapsed = time.time() - _start + log.info(f"pytest finished in {elapsed:.0f}s, returncode={rc}") + + pf = pe = tp = 0 + if junit_xml.exists(): + try: + tree = ET.parse(junit_xml) + root = tree.getroot() + suites = [root] if root.tag != "testsuites" else root.findall("testsuite") + for s in suites: + tp += int(s.get("tests", 0)) + pf += int(s.get("failures", 0)) + pe += int(s.get("errors", 0)) + except Exception: + pass + + passed = pf == 0 and pe == 0 + summary = { + "exit_code": 0 if passed else 1, + "passed": passed, + "test_log": str(junit_xml), + "test_dir": str(test_dir_path), + "passed_count": tp, + "failed_count": pf, + "error_count": pe, + } + (WORKSPACE_DIR / TEST_RESULT_FILE).write_text( + json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + if not passed: + log.error(f"Tests FAILED ({pf} failed, {pe} errors)") + return ctx.copy_with( + test_passed=False, fix_errors=[str(WORKSPACE_DIR / TEST_RESULT_FILE)] + ) + log.status(True, f"All tests passed ({tp} passed)") + return ctx.copy_with(test_passed=True) diff --git a/src/TA_main2main_workflow/scripts/__init__.py b/src/TA_main2main_workflow/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/TA_main2main_workflow/scripts/build_test.py b/src/TA_main2main_workflow/scripts/build_test.py deleted file mode 100644 index ef5fedb..0000000 --- a/src/TA_main2main_workflow/scripts/build_test.py +++ /dev/null @@ -1,637 +0,0 @@ -#!/usr/bin/env python3 -"""Build Triton-Ascend and run tests. - -Build steps: - 1. Check LLVM version and rebuild if needed (unless SKIP_LLVM_REBUILD=true) - 2. Build C++ extensions (CMake / setup.py build) - 3. Install Python package in development mode - 4. Run pre-commit checks (optional) - 5. Run pytest unit tests - -Environment variables: - LLVM_PROJECT_PATH — path to llvm-project repo (default: ~/workspace/llvm-project) - LLVM_INSTALL_PREFIX_SYNC — where to install LLVM (default: ~/workspace/llvm-install-sync) - SKIP_LLVM_REBUILD — set to "true" to skip LLVM rebuild check - -Output: - - workspace/build_result.json - - workspace/test_result.json - - workspace/build.log - - workspace/llvm_build.log -""" - -from __future__ import annotations - -import json -import os -import signal -import subprocess -import sys -import threading -import time -from pathlib import Path - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, BUILD_RESULT_FILE, BUILD_LOG_FILE, TEST_RESULT_FILE, - get_base_branch_ref, -) - - -def _run_to_log(cmd: list[str], cwd: Path, log_path: Path, - env: dict | None = None, timeout: int | None = None, - progress_line: bool = False) -> subprocess.CompletedProcess: - """Run a command, tee output to log file and console. - - Output is streamed to the log file (full) and console (last line with - \\r or every line depending on progress_line). - - If *timeout* is set and the subprocess does not exit within that many - seconds, the entire process group is killed (via os.killpg). Pass - timeout=None (the default) to block indefinitely — suitable for - commands whose runtime is unbounded (e.g. pytest). - - Returns a CompletedProcess with returncode and a pointer to the log. - """ - log_path.parent.mkdir(parents=True, exist_ok=True) - proc_env = os.environ.copy() - if env: - proc_env.update(env) - - print(f" Running: {' '.join(cmd)}") - - # start_new_session=True gives the process its own process group. - # close_fds=True prevents pytest-xdist workers from inheriting the - # parent's file descriptors (especially stdin), which would cause - # the process to hang at ~98% completion. - # On timeout we can kill the entire group (pytest-xdist workers too). - proc = subprocess.Popen( - cmd, cwd=cwd, env=proc_env, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - start_new_session=True, - close_fds=True, - ) - assert proc.stdout is not None - - # ── Read output in a background thread so the main thread can - # enforce the timeout via proc.wait(). When the process is - # killed on timeout the pipe closes, unblocking the reader. ── - last_line: str = "" - read_error: Exception | None = None - - def _reader() -> None: - nonlocal last_line, read_error - try: - with log_path.open("w", encoding="utf-8") as fh: - for line in proc.stdout: - fh.write(line) - if progress_line: - stripped = line.rstrip() - if stripped: - last_line = stripped - # \r returns to line start, \033[K clears residue - print(f"\r {stripped[:120]}\033[K", end="", flush=True) - else: - print(line, end="", flush=True) - except Exception as exc: - read_error = exc - finally: - if progress_line and last_line: - print() # final newline - - reader = threading.Thread(target=_reader, daemon=True) - reader.start() - - timed_out = False - try: - proc.wait(timeout=timeout) - except subprocess.TimeoutExpired: - timed_out = True - print(f"\n ✗ Timeout after {timeout}s — killing process group " - f"(pgid {proc.pid})...") - # Kill the entire process group — catches pytest-xdist workers - # that inherited the session from the main process. - try: - os.killpg(proc.pid, signal.SIGKILL) - except (ProcessLookupError, OSError): - pass - try: - proc.wait(timeout=30) - except subprocess.TimeoutExpired: - print(f" ✗ Process group did not respond to SIGKILL") - - # ── Close stdout pipe to unblock the reader thread ── - # pytest-xdist workers may inherit the write end of the pipe, - # keeping it open after the main process exits. Closing our - # read end forces EOF on the pipe, unblocking the reader. - try: - proc.stdout.close() - except Exception: - pass - - # Wait for the reader thread to finish flushing the last lines - reader.join(timeout=10) - - if read_error: - # Ignore ValueError from closed pipe — this is expected - if not isinstance(read_error, ValueError) or "closed" not in str(read_error).lower(): - print(f" ⚠ Reader thread error: {read_error}") - - if timed_out: - raise subprocess.TimeoutExpired(cmd, timeout) - - if proc.returncode != 0: - print(f" ✗ Exit code: {proc.returncode} (full log: {log_path})") - - return subprocess.CompletedProcess( - cmd, proc.returncode, - stdout="", stderr=f"See {log_path}" - ) - - -def apply_llvm_patches(patch_dir: Path, llvm_project: Path, - target_hash: str = "", - patch_file: Path | None = None) -> dict: - """Apply generated LLVM patch to llvm-project after cleaning stale state. - - 1. Clean any stale modifications in llvm-project (git checkout -- .) - 2. Checkout the target LLVM commit - 3. Apply the patch with 'git apply' - - If *patch_file* is given it is used directly; otherwise - ``patch_dir / "ir_compat.patch"`` is used. - - This is a deterministic operation — no AI involved. - Returns a dict with 'applied', 'failed', 'all_ok'. - """ - if patch_file is None: - patch_file = patch_dir / "ir_compat.patch" - if not patch_file.exists(): - print(f" [llvm-patch] {patch_file.name} not found — nothing to apply") - return {"applied": [], "failed": [], "all_ok": True} - - print(f"\n{'=' * 60}") - print(f" Apply IR compat patch to LLVM") - print(f"{'=' * 60}") - - # ── Step 1: Clean stale modifications ── - print(" [llvm-patch] Cleaning stale changes in llvm-project...") - subprocess.run( - ["git", "checkout", "--", "."], - cwd=llvm_project, capture_output=True, text=True, - ) - subprocess.run( - ["git", "clean", "-fd"], - cwd=llvm_project, capture_output=True, text=True, - ) - print(" [llvm-patch] Working tree cleaned") - - # ── Step 2: Checkout target LLVM commit ── - if target_hash: - print(f" [llvm-patch] Checking out LLVM commit: {target_hash[:12]}") - result = subprocess.run( - ["git", "checkout", target_hash], - cwd=llvm_project, capture_output=True, text=True, - ) - if result.returncode != 0: - print(f" [llvm-patch] FAILED to checkout {target_hash[:12]}: " - f"{result.stderr.strip()[-200:]}") - return {"applied": [], "failed": [{ - "patch": str(patch_file), - "error": f"git checkout failed: {result.stderr.strip()}", - }], "all_ok": False} - print(f" [llvm-patch] Checked out: {target_hash[:12]}") - - # ── Step 3: Apply the patch ── - print(f" [llvm-patch] Applying: {patch_file.name}") - # dry-run first - proc = subprocess.run( - ["git", "apply", "--check", str(patch_file)], - cwd=llvm_project, capture_output=True, text=True, - ) - if proc.returncode != 0: - print(f" [llvm-patch] Patch does NOT apply cleanly:") - print(f" {proc.stderr.strip()[-400:]}") - return {"applied": [], "failed": [{ - "patch": str(patch_file), - "error": proc.stderr.strip(), - }], "all_ok": False} - - result = subprocess.run( - ["git", "apply", str(patch_file)], - cwd=llvm_project, capture_output=True, text=True, - ) - if result.returncode != 0: - print(f" [llvm-patch] FAILED: {result.stderr.strip()[-200:]}") - return {"applied": [], "failed": [{ - "patch": str(patch_file), - "error": result.stderr.strip(), - }], "all_ok": False} - - print(f" [llvm-patch] ✓ Patch applied successfully") - return {"applied": [str(patch_file)], "failed": [], "all_ok": True} - - -def _check_and_rebuild_llvm(repo_path: Path, force_rebuild: bool = False) -> str: - """Check if LLVM version changed and rebuild if needed. - - Reads cmake/llvm-hash.txt from triton-ascend, compares with the - last-built hash stored at {LLVM_INSTALL_PREFIX_SYNC}/.llvm_hash. - If they differ (or no previous build exists), checks out the - required commit in the pre-cloned llvm-project and rebuilds. - - When force_rebuild is True, skips the hash comparison and always - rebuilds. Used after applying IR compatibility patches to LLVM. - - Environment variables: - LLVM_PROJECT_PATH — path to llvm-project (default: ~/llvm-project) - LLVM_INSTALL_PREFIX_SYNC — where to install LLVM (default: ~/llvm-install-sync) - - Returns the LLVM install prefix path. - """ - llvm_project = Path(os.path.expanduser( - os.getenv("LLVM_PROJECT_PATH", "~/llvm-project"))) - llvm_install = Path(os.path.expanduser( - os.getenv("LLVM_INSTALL_PREFIX_SYNC", "~/llvm-install-sync"))) - - # Read the required LLVM hash from triton-ascend - llvm_hash_file = repo_path / "cmake" / "llvm-hash.txt" - if not llvm_hash_file.exists(): - print(f" [llvm] {llvm_hash_file} not found — skipping LLVM rebuild") - return str(llvm_install) - - required_hash = llvm_hash_file.read_text(encoding="utf-8").strip() - if not required_hash: - print(" [llvm] llvm-hash.txt is empty — skipping LLVM rebuild") - return str(llvm_install) - - # Check last-built hash (skip when forcing rebuild) - hash_cache = llvm_install / ".llvm_hash" - if not force_rebuild and hash_cache.exists(): - last_hash = hash_cache.read_text(encoding="utf-8").strip() - if last_hash == required_hash: - print(f" [llvm] LLVM hash unchanged ({required_hash[:12]}) — skip rebuild") - return str(llvm_install) - - if force_rebuild: - print(f"\n{'=' * 60}") - print(f" LLVM force rebuild requested (IR patches applied)") - else: - print(f"\n{'=' * 60}") - print(f" LLVM version changed!") - print(f" Previous: {hash_cache.read_text(encoding='utf-8').strip()[:12] if hash_cache.exists() else '(none)'}") - print(f" Required: {required_hash[:12]}") - print(f" Rebuilding LLVM...") - print(f"{'=' * 60}") - - # Ensure llvm-project exists - if not llvm_project.exists(): - raise RuntimeError( - f"LLVM project not found at {llvm_project}. " - f"Clone it with: git clone https://github.com/llvm/llvm-project.git {llvm_project}" - ) - - # Checkout the required commit (fetch from origin if missing) - proc = subprocess.run( - ["git", "checkout", required_hash], - cwd=llvm_project, capture_output=True, text=True, - ) - if proc.returncode != 0: - print(f" [llvm] Commit {required_hash[:12]} not found locally — fetching from origin...") - for attempt in range(1, 7): - fetch_proc = subprocess.run( - ["git", "fetch", "origin", required_hash], - cwd=llvm_project, capture_output=True, text=True, timeout=2000, - ) - if fetch_proc.returncode == 0: - break - print(f" [llvm] Fetch attempt {attempt}/6 failed — retrying...") - else: - raise RuntimeError( - f"Failed to fetch LLVM commit {required_hash[:12]} after 6 attempts") - _run_cmd( - ["git", "checkout", required_hash], - cwd=llvm_project, - timeout=2000, - ) - - # Clean and rebuild - llvm_build_log = WORKSPACE_DIR / "llvm_build.log" - build_dir = llvm_project / "build" - if build_dir.exists(): - import shutil - shutil.rmtree(build_dir) - build_dir.mkdir() - - cmake_cmd = [ - "cmake", str(llvm_project / "llvm"), - "-G", "Ninja", - "-DCMAKE_BUILD_TYPE=Release", - "-DLLVM_ENABLE_ASSERTIONS=ON", - "-DLLVM_ENABLE_PROJECTS=mlir;llvm;lld", - "-DLLVM_TARGETS_TO_BUILD=host;NVPTX;AMDGPU", - f"-DCMAKE_INSTALL_PREFIX={llvm_install}", - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_CXX_COMPILER=clang++", - ] - print(f" [llvm] Configuring...") - _run_to_log(cmake_cmd, build_dir, llvm_build_log, timeout=300, progress_line=True) - - print(f" [llvm] Building (this may take a while)...") - _run_to_log( - ["ninja", "install"], - build_dir, llvm_build_log, timeout=7200, progress_line=True, - ) - - # Copy FileCheck — not installed by ninja install - filecheck_src = build_dir / "bin" / "FileCheck" - filecheck_dst = llvm_install / "bin" / "FileCheck" - if filecheck_src.exists(): - import shutil - filecheck_dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(filecheck_src, filecheck_dst) - print(f" [llvm] Copied FileCheck to {filecheck_dst}") - else: - print(f" [llvm] WARNING: FileCheck not found at {filecheck_src}") - - # Write the hash cache - llvm_install.mkdir(parents=True, exist_ok=True) - hash_cache.write_text(required_hash, encoding="utf-8") - print(f" [llvm] Rebuild complete — install prefix: {llvm_install}") - - return str(llvm_install) - - -def _run_cmd(cmd: list[str], cwd: Path, timeout: int = 300) -> str: - """Run a command, return stdout. Raise on failure.""" - proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout) - if proc.returncode != 0: - print(f" [llvm] Command failed: {' '.join(cmd)}") - print(f" stderr: {proc.stderr.strip()[-500:]}") - return proc.stdout.strip() - - -def build_triton_ascend( - repo_path: Path, - llvm_prefix: str = "", - conda_env: str = "", - build_dir: str = "build", - clean_build: bool = False, - python_exe: str = "python3", -) -> dict: - """Build the Triton-Ascend C++ extensions and Python package. - - python_exe: Python executable to use for setup.py install - (default 'python3', use 'python3.10' / 'python3.11' for dual tests). - """ - print("\n=== Building Triton-Ascend ===") - - # ── Check and rebuild LLVM if needed ── - skip_llvm = os.getenv("SKIP_LLVM_REBUILD", "false").lower() == "true" - if skip_llvm: - print(" SKIP_LLVM_REBUILD=true — skipping LLVM version check") - else: - resolved_llvm_prefix = _check_and_rebuild_llvm(repo_path) - if resolved_llvm_prefix and not llvm_prefix: - llvm_prefix = resolved_llvm_prefix - - build_log = WORKSPACE_DIR / BUILD_LOG_FILE - - env = {} - if llvm_prefix: - env["LLVM_BUILD_DIR"] = llvm_prefix - env["LLVM_INSTALL_PREFIX"] = llvm_prefix - - steps: list[dict] = [] - all_passed = True - - if clean_build: - build_dir_path = repo_path / build_dir - if build_dir_path.exists(): - print(f" Cleaning build directory: {build_dir_path}") - subprocess.run(["rm", "-rf", str(build_dir_path)], check=False) - steps.append({"step": "clean", "passed": True}) - - print(" Building C++ extensions...") - - # --- Build via setup.py (retained for reference) --- - # build_cmd = [ - # sys.executable, "-m", "pip", "install", "-e", ".", - # "--no-build-isolation", - # ] - - build_env = env.copy() - build_env.update({ - "LLVM_SYSPATH": llvm_prefix, - "TRITON_BUILD_WITH_CCACHE": "true", - "TRITON_BUILD_WITH_CLANG_LLD": "true", - "TRITON_BUILD_PROTON": "OFF", - "DEBUG": "1", - "TRITON_WHEEL_NAME": "triton-ascend", - "TRITON_APPEND_CMAKE_ARGS": "-DTRITON_BUILD_UT=OFF", - }) - build_cmd = [python_exe, "setup.py", "install"] - build_proc = _run_to_log(build_cmd, repo_path, build_log, env=build_env, timeout=1800, progress_line=True) - build_passed = build_proc.returncode == 0 - steps.append({ - "step": "setup_py_install", - "passed": build_passed, - "exit_code": build_proc.returncode, - "log": str(build_log), - }) - if not build_passed: - all_passed = False - print(" Build FAILED!") - else: - # Clear triton cache after a successful build - cache_dir = Path.home() / ".triton" / "cache" - if cache_dir.exists(): - print(f" Clearing triton cache: {cache_dir}") - subprocess.run(["rm", "-rf", str(cache_dir)], check=False) - steps.append({"step": "clear_cache", "passed": True}) - - result = { - "all_passed": all_passed, - "steps": steps, - "build_log": str(build_log), - } - (WORKSPACE_DIR / BUILD_RESULT_FILE).write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - return result - - -def run_tests( - repo_path: Path, - test_dir: str = "third_party/ascend/unittest/pytest_ut", - num_procs: int = 16, - conda_env: str = "", - python_exe: str = "", -) -> dict: - """Run pytest unit tests and return structured results. - - python_exe: Python executable for pytest (default '' uses PYTHON env var - or 'python3'). Set to 'python3.10' / 'python3.11' for dual tests. - """ - print("\n=== Running Tests ===") - test_log_dir = WORKSPACE_DIR / "test-logs" - test_log_dir.mkdir(parents=True, exist_ok=True) - - test_dir_path = repo_path / test_dir - - env = {} - if conda_env: - env["CONDA_DEFAULT_ENV"] = conda_env - - python_exe = python_exe or os.getenv("PYTHON", "python3.10") - - # Resolve to absolute path — test_dir_path may be relative, and the - # subprocess cwd is repo_path. A relative path relative to repo_path - # would double-up (e.g. triton-ascend/triton-ascend/third_party/…). - test_dir_abs = test_dir_path.resolve() - - if not test_dir_abs.exists(): - print(f" WARNING: test directory not found: {test_dir_abs}") - print(f" Skipping tests — directory does not exist after merge.") - passed = False - summary = { - "exit_code": -1, - "passed": False, - "error": f"Test directory not found: {test_dir_abs}", - "test_dir": str(test_dir_abs), - } - else: - # Print bishengir-compile path before running tests - import shutil - bishengir_compile_path = shutil.which("bishengir-compile") - print(f" bishengir-compile: {bishengir_compile_path or 'NOT FOUND'}") - - # JUnit XML for structured result parsing (replaces raw log regex). - junit_xml = test_log_dir / "pytest-junit.xml" - - # Prefer pytest console script; fall back to python -m pytest. - # -s : no capture — stdout/stderr inherit from the terminal. - # pytest-xdist skips its internal IO-thread capture layer, - # avoiding the fork()+IO-thread deadlock that hangs at 97%. - # --junitxml : structured XML report for AI to read test results. - pytest_bin = shutil.which("pytest") - if pytest_bin: - pytest_cmd = [ - pytest_bin, str(test_dir_abs), - "-n", str(num_procs), - # "-sv", - f"--junitxml={junit_xml}", - ] - else: - pytest_cmd = [ - python_exe, "-m", "pytest", - str(test_dir_abs), - "-n", str(num_procs), - # "-sv", - f"--junitxml={junit_xml}", - ] - - proc_env = os.environ.copy() - if env: - proc_env.update(env) - - print(f" cwd: {repo_path}") - print(f" cmd: {' '.join(pytest_cmd)}") - print(f" junitxml: {junit_xml}") - print(f" (stdout inherits terminal — no pipe, no tee, no capture)") - - # Run pytest with a 1000s timeout. - _start = time.time() - _timed_out = False - try: - result = subprocess.run( - pytest_cmd, - cwd=repo_path, env=proc_env, - timeout=1000, - ) - _rc = result.returncode - except subprocess.TimeoutExpired: - _timed_out = True - _rc = -1 - print(f" pytest timed out after 1000s", flush=True) - - _elapsed = time.time() - _start - if not _timed_out: - print(f" pytest finished in {_elapsed:.0f}s, returncode={_rc}") - - # Parse JUnit XML first — real test results take priority over - # process exit status. - _pf = _pe = 0 - _tp = 0 - if junit_xml.exists(): - try: - import xml.etree.ElementTree as ET - tree = ET.parse(junit_xml) - root = tree.getroot() - suites = [root] - if root.tag == "testsuites": - suites = root.findall("testsuite") - for suite in suites: - _tp += int(suite.get("tests", 0)) - _pf += int(suite.get("failures", 0)) - _pe += int(suite.get("errors", 0)) - except Exception: - pass - - # passed = no test failures. Timeout in teardown (all tests - # already finished) is NOT a test failure. - passed = (_pf == 0 and _pe == 0) - - summary = { - "exit_code": 0 if passed else 1, - "passed": passed, - "test_log": str(junit_xml), - "test_dir": str(test_dir_path), - "passed_count": _tp, - "failed_count": _pf, - "error_count": _pe, - } - if _timed_out: - summary["timed_out"] = True - - precommit_config = repo_path / ".pre-commit-config.yaml" - if precommit_config.exists(): - print("\n Running pre-commit checks...") - precommit_log = test_log_dir / "precommit.log" - precommit_passed = True - try: - pc_proc = subprocess.run( - ["pre-commit", "run", "--from-ref", get_base_branch_ref(), "--to-ref", "HEAD"], - cwd=repo_path, - stdout=precommit_log.open("w"), - stderr=subprocess.STDOUT, - timeout=300, - ) - precommit_passed = pc_proc.returncode == 0 - except (subprocess.TimeoutExpired, FileNotFoundError): - precommit_passed = False - - # ── If pre-commit auto-fixed files, amend the latest commit ── - if not precommit_passed: - print(" Pre-commit found issues — checking for auto-fixes...") - from TA_main2main_workflow.utils import run_git_no_check - status_proc = run_git_no_check(repo_path, "status", "--porcelain") - if status_proc.stdout.strip(): - print(" Pre-commit applied auto-fixes, amending commit...") - run_git_no_check(repo_path, "add", "-u") - run_git_no_check(repo_path, "commit", "--amend", "--no-edit") - print(" Commit amended with pre-commit fixes.") - else: - print(" Pre-commit failed but no auto-fixes were applied " - "(manual review may be needed).") - else: - print(" Pre-commit checks passed.") - - summary["precommit_passed"] = precommit_passed - - result_path = WORKSPACE_DIR / TEST_RESULT_FILE - result_path.write_text( - json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - return summary diff --git a/src/TA_main2main_workflow/scripts/detect_commits.py b/src/TA_main2main_workflow/scripts/detect_commits.py deleted file mode 100644 index 2d58aaa..0000000 --- a/src/TA_main2main_workflow/scripts/detect_commits.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -"""Detect the commit gap between triton-ascend and upstream triton. - -For a merge-based workflow (Triton-Ascend is a fork of Triton), we: - 1. Find the merge-base between the current triton-ascend branch and the - upstream triton target commit. - 2. List commits on the upstream side since that merge-base. - 3. Determine total changed files and lines for planning. - -Output: workspace/detect.json -""" - -from __future__ import annotations - -import json -import os -from pathlib import Path - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, DETECT_FILE, run_git, get_repo_head, get_merge_base, - ENV_BASE_BRANCH, get_base_branch_ref, -) - - -def _list_upstream_commits(repo: Path, merge_base: str, target: str) -> list[dict]: - """List commits between merge_base and target, ordered chronologically.""" - log_output = run_git( - repo, "log", "--reverse", "--format=%H%x1f%s", - f"{merge_base}..{target}" - ) - commits: list[dict] = [] - for line in log_output.strip().splitlines(): - if not line.strip(): - continue - parts = line.split("\x1f", 1) - commits.append({ - "sha": parts[0].strip(), - "subject": parts[1].strip() if len(parts) > 1 else "", - }) - return commits - - -def _count_changed_lines(repo: Path, merge_base: str, target: str) -> dict: - """Count changed lines in key source directories.""" - dirs = ["python/triton/", "lib/", "include/", "third_party/nvidia/", "third_party/amd/"] - result = {} - total = 0 - for d in dirs: - try: - output = run_git( - repo, "diff", "--numstat", merge_base, target, "--", f":(top){d}" - ) - except Exception: - result[d] = 0 - continue - lines = 0 - for line in output.strip().splitlines(): - if not line.strip(): - continue - parts = line.split("\t") - if len(parts) >= 3: - added = int(parts[0]) if parts[0] != "-" else 0 - deleted = int(parts[1]) if parts[1] != "-" else 0 - lines += added + deleted - result[d] = lines - total += lines - result["total"] = total - return result - - -def _changed_files(repo: Path, merge_base: str, target: str) -> list[str]: - """Return list of changed files between merge_base and target.""" - output = run_git(repo, "diff", "--name-only", merge_base, target) - return sorted(f for f in output.strip().splitlines() if f) - - -def detect( - triton_ascend_path: Path, - triton_path: Path, - target_commit: str | None = None, -) -> tuple[dict, bool]: - """Detect upstream commits that need to be merged. - - Returns (result_dict, has_new_commits). - """ - # ── Fetch latest from the configured base branch ── - base_branch = os.getenv(ENV_BASE_BRANCH, "main") - base_ref = get_base_branch_ref() - try: - run_git(triton_ascend_path, "fetch", "origin", base_branch) - print(f"[detect] Fetched {base_ref} (private fork)") - except Exception: - print(f"[detect] Warning: could not fetch {base_ref}, using local refs") - - # ── Fetch latest from upstream-triton ── - if not target_commit: - try: - run_git(triton_ascend_path, "fetch", "upstream-triton", "--prune") - except Exception: - print("[detect] Warning: could not fetch upstream-triton, using local refs") - - # ── Use the configured base branch as the ascend reference (not checkout HEAD) ── - # The work branch will be created from the base branch, so the merge_base - # must be computed against it — otherwise we'd include commits - # that are already on the base branch. - try: - ascend_head = run_git(triton_ascend_path, "rev-parse", base_ref).strip() - except Exception: - ascend_head = get_repo_head(triton_ascend_path) - print(f"[detect] Warning: {base_ref} not available, using checkout HEAD") - - target = target_commit if target_commit else get_repo_head(triton_path) - - # ── Debug: print key refs ── - checkout_head = get_repo_head(triton_ascend_path) - print(f"[detect] Checkout HEAD : {checkout_head[:12]}") - print(f"[detect] {base_ref} : {ascend_head[:12]}") - print(f"[detect] upstream target: {target[:12]}") - - merge_base = get_merge_base(triton_ascend_path, ascend_head, target) - print(f"[detect] merge_base : {merge_base[:12]}") - - commits = _list_upstream_commits(triton_path, merge_base, target) - has_new = len(commits) > 0 and merge_base != target - - if has_new: - print(f"[detect] {len(commits)} new upstream commits to merge " - f"({commits[0]['sha'][:8]}..{commits[-1]['sha'][:8]})") - else: - print("[detect] No new upstream commits — already up to date") - - result = { - "ascend_head": ascend_head, - "target_commit": target, - "merge_base": merge_base, - "upstream_commits_count": len(commits), - "upstream_commits": commits, - "changed_lines": _count_changed_lines(triton_path, merge_base, target), - "changed_files": _changed_files(triton_path, merge_base, target), - "changed_files_count": len(_changed_files(triton_path, merge_base, target)), - } - - (WORKSPACE_DIR / DETECT_FILE).write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - return result, has_new diff --git a/src/TA_main2main_workflow/scripts/merge_upstream.py b/src/TA_main2main_workflow/scripts/merge_upstream.py deleted file mode 100644 index fbeb400..0000000 --- a/src/TA_main2main_workflow/scripts/merge_upstream.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/env python3 -"""Perform git merge of upstream triton commits into triton-ascend work branch. - -Creates a work branch based on the latest main from triton-lang/triton-ascend -(fetched fresh each run), then merges the target upstream commit. -If merge conflicts occur, they are recorded for later AI resolution. - -Output: - - workspace/merge_result.json - - workspace/merge.log (raw git merge output) -""" - -from __future__ import annotations - -import json -import os -import subprocess -from datetime import datetime -from pathlib import Path - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, MERGE_RESULT_FILE, MERGE_LOG_FILE, CONFLICT_LOG_DIR, - run_git, run_git_no_check, has_merge_conflicts, get_conflict_files, - ENV_BASE_BRANCH, get_base_branch_ref, -) - - -def _check_tracked_changes(repo: Path) -> bool: - """Return True if tracked files have uncommitted changes (modified or staged).""" - unstaged = run_git_no_check(repo, "diff", "--quiet") - staged = run_git_no_check(repo, "diff", "--cached", "--quiet") - return unstaged.returncode != 0 or staged.returncode != 0 - - -def _auto_stash(repo: Path) -> str: - """Stash all changes (including untracked). Returns the stash name.""" - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - name = f"ta-sync-{ts}" - run_git(repo, "stash", "push", "-u", "-m", name) - print(f"[merge] Auto-stashed changes as '{name}'") - return name - - -def _abort_stale_merge(repo: Path) -> None: - """Abort any stale merge in progress.""" - merge_head = repo / ".git" / "MERGE_HEAD" - if merge_head.exists(): - print("[merge] Found stale MERGE_HEAD, running git merge --abort") - try: - run_git(repo, "merge", "--abort") - except subprocess.CalledProcessError: - print("[merge] Warning: git merge --abort failed, trying git reset --hard HEAD") - run_git(repo, "reset", "--hard", "HEAD") - for f in [".git/MERGE_MODE", ".git/MERGE_MSG", ".git/CHERRY_PICK_HEAD"]: - p = repo / f - if p.exists(): - p.unlink() - - -def _ensure_upstream_ascend_remote(repo: Path) -> str: - """Ensure a remote for triton-lang/triton-ascend exists and return its name. - - Checks existing remotes for one that points to triton-lang/triton-ascend. - If none found, adds a remote named 'upstream-ascend'. - Returns the remote name to use for fetching. - """ - ASCEND_UPSTREAM_URL = "https://github.com/triton-lang/triton-ascend.git" - - # Check if any existing remote already points to the ascend upstream - remotes_proc = run_git_no_check(repo, "remote", "-v") - for line in remotes_proc.stdout.strip().splitlines(): - if ASCEND_UPSTREAM_URL in line: - remote_name = line.split()[0] - print(f"[merge] Found existing remote '{remote_name}' → {ASCEND_UPSTREAM_URL}") - return remote_name - - # Not found — add a new remote - remote_name = "upstream-ascend" - print(f"[merge] Adding remote '{remote_name}' → {ASCEND_UPSTREAM_URL}") - run_git(repo, "remote", "add", remote_name, ASCEND_UPSTREAM_URL) - return remote_name - - -def _create_work_branch(repo: Path, suffix: str = "") -> str: - """Create and checkout a work branch for the merge. - - Default: branch from triton-lang/triton-ascend/upstream-sync. - Set TA_WORK_BRANCH_BASE=origin to branch from the local fork instead, - and TA_BASE_BRANCH to change the branch name on either remote. - """ - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - branch = f"auto/upstream-sync-{ts}{'-' + suffix if suffix else ''}" - - if _check_tracked_changes(repo): - auto_stash = os.getenv("AUTO_STASH", "false").lower() == "true" - if auto_stash: - _auto_stash(repo) - else: - print("[merge] ERROR: Tracked files have uncommitted changes.") - print("[merge] Hint: git stash push -u -m 'pre-sync-stash'") - print("[merge] or set AUTO_STASH=true to auto-stash before sync") - raise RuntimeError( - "Working tree has uncommitted changes to tracked files. " - "Commit or stash changes before running sync." - ) - - _abort_stale_merge(repo) - - # Base branch for work branches. Defaults to the upstream repo's - # upstream-sync branch (triton-lang/triton-ascend). Set - # TA_WORK_BRANCH_BASE=origin to branch from the local fork instead. - _upstream_branch = os.getenv(ENV_BASE_BRANCH, "upstream-sync") - branch_base = os.getenv("TA_WORK_BRANCH_BASE", "upstream-ascend") - - if branch_base == "upstream-ascend": - upstream_remote = _ensure_upstream_ascend_remote(repo) - print(f"[merge] Fetching latest {_upstream_branch} from " - f"'{upstream_remote}'...") - run_git(repo, "fetch", upstream_remote, _upstream_branch) - base_ref = f"{upstream_remote}/{_upstream_branch}" - else: - base_ref = get_base_branch_ref() - base_branch = os.getenv(ENV_BASE_BRANCH, "main") - print(f"[merge] Fetching latest {base_ref} from origin...") - try: - run_git(repo, "fetch", "origin", base_branch) - except Exception: - print(f"[merge] Warning: could not fetch {base_ref}, using local ref") - - # ── Reset to the pristine base ref and clean the working tree ── - # This guarantees the work branch starts from exactly the right - # commit, with no leftover artifacts from previous runs. - print(f"[merge] Resetting working tree to {base_ref}...") - run_git(repo, "checkout", "--detach", base_ref) - run_git(repo, "reset", "--hard", "HEAD") - run_git(repo, "clean", "-fd") - - # Resolve base ref to a commit so we can log both the name and the SHA - base_sha = run_git(repo, "rev-parse", base_ref).strip() - print(f"[merge] Base branch: {base_ref} commit: {base_sha[:12]}") - - print(f"[merge] Creating work branch '{branch}' from {base_ref}") - proc = run_git_no_check(repo, "checkout", "-B", branch, base_ref) - if proc.returncode != 0: - print(f"[merge] ERROR: git checkout -B {branch} {base_ref} failed") - print(f"[merge] stderr: {proc.stderr.strip()}") - raise RuntimeError(f"Failed to create work branch '{branch}': {proc.stderr.strip()}") - - print(f"[merge] Created work branch: {branch} (based on {base_ref})") - return branch - - -def _get_conflict_content(repo: Path, filepath: str) -> str: - """Get the content of a conflicted file (with conflict markers).""" - file_path = Path(repo) / filepath - if file_path.exists(): - return file_path.read_text(encoding="utf-8", errors="replace") - return "" - - -def _save_conflict_info(repo: Path, conflict_files: list[str], log_dir: Path) -> list[dict]: - """Save conflict file contents and return structured conflict info.""" - conflicts = [] - for f in conflict_files: - content = _get_conflict_content(repo, f) - conflict_file = log_dir / f"{f.replace('/', '_')}.conflict" - conflict_file.parent.mkdir(parents=True, exist_ok=True) - conflict_file.write_text(content, encoding="utf-8") - conflicts.append({ - "file": f, - "conflict_snapshot": str(conflict_file), - "size_bytes": len(content), - }) - return conflicts - - -def run_merge( - triton_ascend_path: Path, - triton_path: Path, - target_commit: str, -) -> dict: - """Merge upstream triton *target_commit* into triton-ascend. - - Returns a dict with merge status, branch name, conflict info. - """ - ascend_path = Path(triton_ascend_path) - - original_branch = run_git(ascend_path, "branch", "--show-current").strip() - if not original_branch: - original_branch = run_git(ascend_path, "rev-parse", "HEAD").strip() - - work_branch = _create_work_branch(ascend_path) - - try: - run_git(ascend_path, "fetch", "upstream-triton", "--prune") - except subprocess.CalledProcessError: - print("[merge] Warning: could not fetch upstream-triton, assuming target is reachable") - - if Path(triton_path) != ascend_path: - try: - run_git(ascend_path, "fetch", str(triton_path), target_commit) - except subprocess.CalledProcessError: - print("[merge] Warning: could not fetch target from triton path") - - print(f"[merge] Merging {target_commit[:12]} into {work_branch}") - merge_proc = run_git_no_check( - ascend_path, "merge", "--no-ff", "--no-edit", target_commit - ) - - # Use a timestamped log file so each merge step's output is preserved - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - merge_log_path = WORKSPACE_DIR / f"merge-{ts}.log" - merge_log_path.write_text( - f"STDOUT:\n{merge_proc.stdout}\n\nSTDERR:\n{merge_proc.stderr}\n", - encoding="utf-8", - ) - # Also write/update the canonical merge log for quick access to the latest - (WORKSPACE_DIR / MERGE_LOG_FILE).write_text( - f"STDOUT:\n{merge_proc.stdout}\n\nSTDERR:\n{merge_proc.stderr}\n", - encoding="utf-8", - ) - - has_conflicts = has_merge_conflicts(ascend_path) - conflict_files = get_conflict_files(ascend_path) if has_conflicts else [] - - conflict_dir = WORKSPACE_DIR / CONFLICT_LOG_DIR - conflict_info = [] - if has_conflicts: - conflict_dir.mkdir(parents=True, exist_ok=True) - conflict_info = _save_conflict_info(ascend_path, conflict_files, conflict_dir) - - result = { - "work_branch": work_branch, - "original_branch": original_branch, - "target_commit": target_commit, - "merge_exit_code": merge_proc.returncode, - "has_conflicts": has_conflicts, - "conflict_files": conflict_files, - "conflict_count": len(conflict_files), - "conflicts": conflict_info, - "merge_log": str(merge_log_path), - "conflict_dir": str(conflict_dir) if has_conflicts else "", - } - - result_path = WORKSPACE_DIR / f"merge_result-{ts}.json" - result_path.write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - # Also write/update the canonical result for quick access to the latest - (WORKSPACE_DIR / MERGE_RESULT_FILE).write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - return result - - -def run_merge_incremental( - triton_ascend_path: Path, - triton_path: Path, - target_commit: str, - work_branch: str, -) -> dict: - """Merge *target_commit* into an already-existing work branch. - - Used for progressive step-by-step merging: the first step calls - run_merge() to create the work branch, and subsequent steps call - run_merge_incremental() to merge their end_commit on top. - - Does NOT create a new branch or stash changes — it assumes we're - already on the work branch from a previous step. - """ - ascend_path = Path(triton_ascend_path) - - # Verify we're on the expected work branch - current_branch = run_git(ascend_path, "branch", "--show-current").strip() - if current_branch != work_branch: - print(f"[merge] Switching from '{current_branch}' to work branch '{work_branch}'") - run_git(ascend_path, "checkout", work_branch) - - # Fetch the target commit if needed - try: - run_git(ascend_path, "fetch", "upstream-triton", "--prune") - except subprocess.CalledProcessError: - print("[merge] Warning: could not fetch upstream-triton, assuming target is reachable") - - if Path(triton_path) != ascend_path: - try: - run_git(ascend_path, "fetch", str(triton_path), target_commit) - except subprocess.CalledProcessError: - print("[merge] Warning: could not fetch target from triton path") - - print(f"[merge] Incremental merge {target_commit[:12]} into {work_branch}") - merge_proc = run_git_no_check( - ascend_path, "merge", "--no-ff", "--no-edit", target_commit - ) - - # Use a timestamped log file so each merge step's output is preserved - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - merge_log_path = WORKSPACE_DIR / f"merge-{ts}.log" - merge_log_path.write_text( - f"STDOUT:\n{merge_proc.stdout}\n\nSTDERR:\n{merge_proc.stderr}\n", - encoding="utf-8", - ) - # Also write/update the canonical merge log for quick access to the latest - (WORKSPACE_DIR / MERGE_LOG_FILE).write_text( - f"STDOUT:\n{merge_proc.stdout}\n\nSTDERR:\n{merge_proc.stderr}\n", - encoding="utf-8", - ) - - has_conflicts = has_merge_conflicts(ascend_path) - conflict_files = get_conflict_files(ascend_path) if has_conflicts else [] - - conflict_dir = WORKSPACE_DIR / CONFLICT_LOG_DIR - conflict_info = [] - if has_conflicts: - conflict_dir.mkdir(parents=True, exist_ok=True) - conflict_info = _save_conflict_info(ascend_path, conflict_files, conflict_dir) - - result = { - "work_branch": work_branch, - "original_branch": current_branch, - "target_commit": target_commit, - "merge_exit_code": merge_proc.returncode, - "has_conflicts": has_conflicts, - "conflict_files": conflict_files, - "conflict_count": len(conflict_files), - "conflicts": conflict_info, - "merge_log": str(merge_log_path), - "conflict_dir": str(conflict_dir) if has_conflicts else "", - } - - result_path = WORKSPACE_DIR / f"merge_result-{ts}.json" - result_path.write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - # Also write/update the canonical result for quick access to the latest - (WORKSPACE_DIR / MERGE_RESULT_FILE).write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - return result diff --git a/src/TA_main2main_workflow/scripts/plan_steps.py b/src/TA_main2main_workflow/scripts/plan_steps.py deleted file mode 100644 index 37ffc80..0000000 --- a/src/TA_main2main_workflow/scripts/plan_steps.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/env python3 -"""Deterministic step planner for the TA main2main upstream sync pipeline. - -Splits a range of upstream Triton commits into ordered steps based on changed -lines in key source directories. Every commit between base and target is -included — no commits are skipped, including those that touch zero source -lines (they are still tracked but contribute 0 to the line budget). - -Algorithm (in priority order): - 1. LLVM version change → solo step: - If a commit modifies cmake/llvm-hash.txt it MUST be merged alone, - regardless of its source-line count. Pending commits are flushed first. - 2. Oversized single commit: - A commit whose source lines exceed LINE_BUDGET becomes its own step. - 3. Line-budget grouping: - Commits accumulate into a step until source_changed_lines > LINE_BUDGET - (no commit-count limit — as many commits as fit within the line budget). - -The LINE_BUDGET can be controlled via TA_LINE_BUDGET env var (default: 1000). - -Output: - - /steps.json — machine-readable plan - - /steps//upstream.patch — per-step upstream diff - - /steps//changed_files.txt — per-step changed files -""" - -from __future__ import annotations - -import json -import os -from pathlib import Path -from typing import Any - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, STEPS_FILE, STEPS_DIR, LINE_BUDGET, SOURCE_DIRS, - LLVM_HASH_FILE, run_git, -) - - -def _list_commits(repo: Path, base: str, target: str) -> list[dict[str, str]]: - """List all commits between base and target, ordered chronologically.""" - log_output = run_git( - repo, "log", "--reverse", "--format=%H%x1f%s", f"{base}..{target}" - ) - commits: list[dict[str, str]] = [] - for line in log_output.strip().splitlines(): - if not line.strip(): - continue - parts = line.split("\x1f", 1) - commits.append({ - "sha": parts[0].strip(), - "subject": parts[1].strip() if len(parts) > 1 else "", - }) - return commits - - -def _source_lines_for_commit(repo: Path, sha: str) -> int: - """Count changed lines in SOURCE_DIRS for a single commit using diff-tree.""" - total = 0 - for src_dir in SOURCE_DIRS: - try: - output = run_git( - repo, "diff-tree", "--no-commit-id", "-r", "--numstat", - sha, "--", f":(top){src_dir}", - ) - except Exception: - continue - for line in output.strip().splitlines(): - if not line.strip(): - continue - parts = line.split("\t") - if len(parts) >= 3: - added = int(parts[0]) if parts[0] != "-" else 0 - deleted = int(parts[1]) if parts[1] != "-" else 0 - total += added + deleted - return total - - -def _commit_changed_llvm_hash(repo: Path, sha: str) -> bool: - """Check if a single commit modified cmake/llvm-hash.txt. - - Uses git diff-tree to list files changed by *sha*, then checks whether - LLVM_HASH_FILE appears in the output. A commit that touches this file - must become a solo step regardless of its source-line count. - """ - try: - output = run_git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", sha) - return LLVM_HASH_FILE in output - except Exception: - return False - - -def _make_step( - index: int, - commits: list[dict[str, str]], - start_commit: str, - total_lines: int, - line_budget: int, - reason: str = "line_budget", -) -> dict[str, Any]: - """Build a step dict from accumulated commits. - - The 'commits' field stores objects with 'sha' and 'subject' keys, - matching the vllm-ascend main2main_flow format. - - *reason* explains why this step was formed: - - ``"line_budget"`` — normal grouping by line budget - - ``"llvm_version"`` — solo step because commit changed llvm-hash.txt - - ``"oversized"`` — solo step because a single commit exceeds budget - """ - return { - "index": index, - "id": f"step-{index}", - "commits": commits, # list of {"sha": ..., "subject": ...} - "commit_count": len(commits), - "start_commit": start_commit, - "end_commit": commits[-1]["sha"], - "source_changed_lines": total_lines, - "line_budget": line_budget, - "reason": reason, - } - - -def _plan_steps( - commits: list[dict[str, str]], - lines_per_commit: dict[str, int], - base_commit: str, - line_budget: int = LINE_BUDGET, - llvm_commits: set[str] | None = None, -) -> list[dict[str, Any]]: - """Group commits into steps with LLVM-aware planning. - - Every commit in the range is included — even those that touch zero - source lines (they contribute 0 to the line budget and don't cause - step splits on their own). - - Algorithm (in priority order): - 1. **LLVM version change → solo step**: If a commit modifies - ``cmake/llvm-hash.txt`` it MUST be merged alone, regardless of - its source-line count. Pending commits are flushed first. - 2. **Oversized single commit**: A commit whose source lines exceed - LINE_BUDGET becomes its own step. - 3. **Line-budget grouping**: Otherwise accumulate commits until - ``step_lines + commit_lines > line_budget``, then flush. - No commit-count cap — as many commits as fit within the budget. - """ - if llvm_commits is None: - llvm_commits = set() - - steps: list[dict[str, Any]] = [] - step_commits: list[dict[str, str]] = [] - step_lines = 0 - start = base_commit - - for commit in commits: - sha = commit["sha"] - lines = lines_per_commit.get(sha, 0) - is_llvm_change = sha in llvm_commits - - # ── Rule 1.1: LLVM version change → solo step ── - if is_llvm_change: - if step_commits: - steps.append(_make_step( - len(steps) + 1, step_commits, start, step_lines, - line_budget, reason="line_budget", - )) - start = steps[-1]["end_commit"] - step_commits = [] - step_lines = 0 - steps.append(_make_step( - len(steps) + 1, [commit], start, lines, line_budget, - reason="llvm_version", - )) - start = steps[-1]["end_commit"] - continue - - # ── Rule 2.1: Oversized single commit → solo step ── - if lines > line_budget: - if step_commits: - steps.append(_make_step( - len(steps) + 1, step_commits, start, step_lines, - line_budget, reason="line_budget", - )) - start = steps[-1]["end_commit"] - step_commits = [] - step_lines = 0 - steps.append(_make_step( - len(steps) + 1, [commit], start, lines, line_budget, - reason="oversized", - )) - start = steps[-1]["end_commit"] - continue - - # ── Would exceed line budget → flush current step first ── - if step_lines + lines > line_budget: - steps.append(_make_step( - len(steps) + 1, step_commits, start, step_lines, - line_budget, reason="line_budget", - )) - start = steps[-1]["end_commit"] - step_commits = [] - step_lines = 0 - - step_commits.append(commit) - step_lines += lines - - # ── Flush remaining ── - if step_commits: - steps.append(_make_step( - len(steps) + 1, step_commits, start, step_lines, - line_budget, reason="line_budget", - )) - - return steps - - -def _enrich_steps_with_diff(triton_path: Path, steps: list[dict[str, Any]]) -> None: - """Add upstream diff and changed file list to each step. - - Filters to SOURCE_DIRS only so each step's patch is scoped to the - code that actually needs adaptation (python/triton/, lib/, include/). - Matches vllm-ascend's approach of filtering to vllm/. - """ - # Build pathspec arg for git diff filtering: :(top)python/triton/ :(top)lib/ :(top)include/ - pathspec_args: list[str] = [] - for d in SOURCE_DIRS: - pathspec_args.extend(["--", f":(top){d}"]) - - for step in steps: - step["upstream_patch"] = run_git( - triton_path, "diff", - f"{step['start_commit']}..{step['end_commit']}", - *pathspec_args, - ) - changed_files = run_git( - triton_path, "diff", "--name-only", - f"{step['start_commit']}..{step['end_commit']}", - *pathspec_args, - ) - step["changed_files"] = changed_files - step["files_changed"] = sorted( - f for f in changed_files.strip().splitlines() if f - ) - - -def run_plan( - triton_path: Path, - base_commit: str, - target_commit: str, - line_budget: int | None = None, -) -> dict[str, Any]: - """Main entry point: plan steps and write steps.json + per-step artifacts. - - Args: - triton_path: Path to the upstream Triton git repository. - base_commit: Merge-base commit (start of the range). - target_commit: Target upstream commit (end of the range). - line_budget: Max source lines per step. Reads TA_LINE_BUDGET env var - if omitted, falls back to LINE_BUDGET (1000). - - Steps are determined solely by the line budget — there is no - commit-count limit. All commits between base and target are included. - - Returns: - Plan dict with keys: base_commit, target_commit, total_commits, steps. - """ - if line_budget is None: - line_budget = int(os.getenv("TA_LINE_BUDGET", str(LINE_BUDGET))) - - commits = _list_commits(triton_path, base_commit, target_commit) - - print(f"[plan] Scanning {len(commits)} upstream commits " - f"({base_commit[:8]}..{target_commit[:8]})") - print(f"[plan] Line budget: {line_budget} (no commit-count limit)") - - # Count changed source lines per commit + detect LLVM version changes - lines_per_commit: dict[str, int] = {} - llvm_commits: set[str] = set() - source_touching_count = 0 - for i, c in enumerate(commits): - lines = _source_lines_for_commit(triton_path, c["sha"]) - lines_per_commit[c["sha"]] = lines - if lines > 0: - source_touching_count += 1 - # Rule 1.1: check if this commit changed cmake/llvm-hash.txt - if _commit_changed_llvm_hash(triton_path, c["sha"]): - llvm_commits.add(c["sha"]) - print(f"[plan] LLVM version change detected: {c['sha'][:8]} {c['subject'][:80]}") - if (i + 1) % 50 == 0: - print(f"[plan] ... scanned {i + 1}/{len(commits)} commits") - - if source_touching_count < len(commits): - print(f"[plan] {len(commits) - source_touching_count} commits touch zero " - f"source lines — included in steps with 0 line contribution") - - if llvm_commits: - print(f"[plan] {len(llvm_commits)} commit(s) changed LLVM hash " - f"— each will be a solo merge step") - - steps = _plan_steps(commits, lines_per_commit, base_commit, line_budget, - llvm_commits=llvm_commits) - _enrich_steps_with_diff(triton_path, steps) - - plan = { - "base_commit": base_commit, - "target_commit": target_commit, - "line_budget": line_budget, - "total_source_commits": source_touching_count, - "total_commits": sum(s["commit_count"] for s in steps), - "total_steps": len(steps), - "steps": steps, - } - - # ── Write steps.json ── - steps_dir = WORKSPACE_DIR / STEPS_DIR - steps_dir.mkdir(parents=True, exist_ok=True) - (WORKSPACE_DIR / STEPS_FILE).write_text( - json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - # ── Write per-step artifacts ── - for step in steps: - step_dir = steps_dir / step["id"] - step_dir.mkdir(parents=True, exist_ok=True) - (step_dir / "upstream.patch").write_text( - step["upstream_patch"], encoding="utf-8" - ) - (step_dir / "changed_files.txt").write_text( - step["changed_files"], encoding="utf-8" - ) - # Write a human-readable commit list for this step - commit_list_lines = [] - for c in step["commits"]: - commit_list_lines.append(f"{c['sha'][:8]} {c['subject']}") - (step_dir / "commits.txt").write_text( - "\n".join(commit_list_lines) + "\n", encoding="utf-8" - ) - - print(f"[plan] Generated {len(steps)} step(s) totaling " - f"{plan['total_commits']} source-touching commits") - for s in steps: - reason_tag = "" - if s.get("reason") == "llvm_version": - reason_tag = " [LLVM VERSION]" - elif s.get("reason") == "oversized": - reason_tag = " [OVERSIZED]" - print(f" {s['id']}: {s['commit_count']} commits, " - f"{s['source_changed_lines']} lines " - f"({'OVERSIZED' if s['source_changed_lines'] > line_budget else 'OK'})" - f"{reason_tag}") - - return plan - - -def plan_steps( - triton_path: Path, - base_commit: str, - target_commit: str, - line_budget: int | None = None, -) -> list[dict[str, Any]]: - """Public wrapper: plan steps and return the step list (for testing). - - Same as run_plan() but returns just the steps list instead of the full - plan dict. Does NOT write files to disk — call run_plan() for that. - """ - if line_budget is None: - line_budget = int(os.getenv("TA_LINE_BUDGET", str(LINE_BUDGET))) - - commits = _list_commits(triton_path, base_commit, target_commit) - - lines_per_commit: dict[str, int] = {} - llvm_commits: set[str] = set() - for c in commits: - lines = _source_lines_for_commit(triton_path, c["sha"]) - lines_per_commit[c["sha"]] = lines - if _commit_changed_llvm_hash(triton_path, c["sha"]): - llvm_commits.add(c["sha"]) - - return _plan_steps(commits, lines_per_commit, base_commit, line_budget, - llvm_commits=llvm_commits) diff --git a/src/TA_main2main_workflow/scripts/pre_ci_check.py b/src/TA_main2main_workflow/scripts/pre_ci_check.py deleted file mode 100644 index 0f67183..0000000 --- a/src/TA_main2main_workflow/scripts/pre_ci_check.py +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env python3 -"""Pre-CI verification for TA_main2main sync steps. - -Runs mechanical checks before build/test to catch common issues early: - 1. Merge conflict marker check: no remaining <<<<<<< / ======= / >>>>>>> markers - 2. Python syntax check: quick syntax validation on modified .py files - -Also provides cleanup_temp_files() to actively remove test artifacts -(result_profiling/, *.lock, __pycache__/, *.pyc) before committing. - -All results are printed to the local console and written to workspace. -""" - -from __future__ import annotations - -import ast -import json -import subprocess -from pathlib import Path - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, PRE_CI_CHECK_FILE, run_git_no_check, - print_section, print_status, print_info, print_warn, -) - -# Directories to purge (recursively removed if found under repo root) -_CLEANUP_DIRS = [ - "result_profiling", - "__pycache__", - ".pytest_cache", - ".mypy_cache", - "*.egg-info", -] - -# File patterns to purge (matched via glob **/*.suffix and exact basename) -_CLEANUP_SUFFIXES = [ - ".lock", - ".pyc", - ".pyo", - ".orig", # git merge conflict backups - ".rej", # patch rejection files - ".log", # log files that may leak into repo -] - -_CLEANUP_BASENAMES = [ - ".DS_Store", -] - -_CONFLICT_MARKERS = [ - "<<<<<<<", - "=======", - ">>>>>>>", -] - - -def _get_modified_files(repo: Path) -> list[str]: - """Return list of modified (unstaged + staged) files.""" - modified: set[str] = set() - - result = run_git_no_check(repo, "diff", "--name-only", "HEAD") - if result.stdout.strip(): - modified.update(result.stdout.strip().splitlines()) - - result = run_git_no_check(repo, "diff", "--name-only", "--cached") - if result.stdout.strip(): - modified.update(result.stdout.strip().splitlines()) - - result = run_git_no_check(repo, "ls-files", "--others", "--exclude-standard") - if result.stdout.strip(): - modified.update(result.stdout.strip().splitlines()) - - return sorted(modified) - - -def _check_conflict_markers(repo: Path, modified_files: list[str]) -> dict: - """Scan modified files for remaining merge conflict markers.""" - violations: list[dict] = [] - for filepath in modified_files: - full_path = repo / filepath - if not full_path.exists() or not full_path.is_file(): - continue - try: - content = full_path.read_text(encoding="utf-8", errors="replace") - except Exception: - continue - for lineno, line in enumerate(content.splitlines(), 1): - for marker in _CONFLICT_MARKERS: - if line.strip().startswith(marker): - violations.append({ - "file": filepath, - "line": lineno, - "marker": marker, - "text": line.strip()[:120], - }) - - return { - "name": "conflict_markers", - "passed": len(violations) == 0, - "violations": violations, - "detail": ( - "no remaining conflict markers" - if len(violations) == 0 - else f"{len(violations)} conflict marker(s) still present" - ), - } - - -def cleanup_temp_files(repo: Path) -> dict: - """Actively remove test artifacts and temp files from the repository. - - Deletes directories like result_profiling/, __pycache__/ and files - matching *.lock, *.pyc, etc. This prevents them from being accidentally - committed via git add -u or git add -A. - - Only operates inside the repo (not outside it). Uses a whitelist of - known-temp patterns — it will NOT delete arbitrary files. - - Returns a dict with counts of what was cleaned. - """ - import shutil - - removed_dirs: list[str] = [] - removed_files: list[str] = [] - - # ── Remove matching directories (recursively from repo root) ── - for dirname in _CLEANUP_DIRS: - for found in repo.rglob(dirname): - if found.is_dir() and ".git" not in found.parts: - try: - shutil.rmtree(found, ignore_errors=True) - removed_dirs.append(str(found.relative_to(repo))) - except Exception: - pass - - # ── Remove files by suffix ── - for suffix in _CLEANUP_SUFFIXES: - for found in repo.rglob(f"*{suffix}"): - if found.is_file() and ".git" not in found.parts: - try: - found.unlink() - removed_files.append(str(found.relative_to(repo))) - except Exception: - pass - - # ── Remove files by exact basename ── - for name in _CLEANUP_BASENAMES: - for found in repo.rglob(name): - if found.is_file() and ".git" not in found.parts: - try: - found.unlink() - removed_files.append(str(found.relative_to(repo))) - except Exception: - pass - - total = len(removed_dirs) + len(removed_files) - if total > 0: - print_info(f"Cleaned up {total} temp artifact(s):") - for d in removed_dirs: - print_info(f" rmdir: {d}") - for f in removed_files: - print_info(f" rm: {f}") - else: - print_info("No temp artifacts to clean up") - - return { - "name": "cleanup_temp_files", - "passed": True, - "removed_dirs": removed_dirs, - "removed_files": removed_files, - "total_removed": total, - } - - -def _check_python_syntax(repo: Path, modified_files: list[str]) -> dict: - """Quick Python syntax check on modified .py files.""" - violations: list[dict] = [] - py_files = [f for f in modified_files if f.endswith(".py")] - - for filepath in py_files: - full_path = repo / filepath - if not full_path.exists(): - continue - try: - source = full_path.read_text(encoding="utf-8") - ast.parse(source, filename=filepath) - except SyntaxError as e: - violations.append({ - "file": filepath, - "line": e.lineno or 0, - "msg": str(e.msg), - }) - except Exception: - pass - - return { - "name": "python_syntax", - "passed": len(violations) == 0, - "violations": violations, - "detail": ( - f"all {len(py_files)} modified .py files pass syntax check" - if len(violations) == 0 - else f"{len(violations)} file(s) have syntax errors" - ), - } - - -def run_pre_ci_check(repo: Path, step_id: str = "") -> dict: - """Run all pre-CI checks on the triton-ascend working tree. - - Returns a dict with 'all_passed' (bool) and 'checks' (list of check results). - """ - print_section(f"Pre-CI Check{f' — {step_id}' if step_id else ''}") - - try: - modified_files = _get_modified_files(repo) - except subprocess.CalledProcessError as exc: - print_warn(f"Could not list modified files: {exc.stderr}") - return {"all_passed": True, "checks": [], "error": str(exc.stderr)} - - if not modified_files: - print_info("No modified files — nothing to check") - return {"all_passed": True, "checks": [], "modified_files_count": 0} - - print_info(f"Checking {len(modified_files)} modified file(s)") - - # ── Phase 0: active cleanup of known temp artifacts ── - cleanup_temp_files(repo) - # Re-scan modified files after cleanup (some may have been removed) - try: - modified_files = _get_modified_files(repo) - except subprocess.CalledProcessError: - pass - - checks: list[dict] = [] - all_passed = True - - conflict_check = _check_conflict_markers(repo, modified_files) - checks.append(conflict_check) - print_status(conflict_check["passed"], conflict_check["detail"]) - if not conflict_check["passed"]: - all_passed = False - for v in conflict_check["violations"]: - print_warn(f" {v['file']}:{v['line']} — {v['marker']}") - - syntax_check = _check_python_syntax(repo, modified_files) - checks.append(syntax_check) - print_status(syntax_check["passed"], syntax_check["detail"]) - if not syntax_check["passed"]: - all_passed = False - for v in syntax_check["violations"]: - print_warn(f" {v['file']}:{v['line']} — {v['msg']}") - - if all_passed: - print_status(True, "All pre-CI checks passed") - else: - print_status(False, "Pre-CI checks found issues") - - result = { - "all_passed": all_passed, - "checks": checks, - "modified_files_count": len(modified_files), - } - - check_path = WORKSPACE_DIR / PRE_CI_CHECK_FILE - check_path.write_text( - json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - return result diff --git a/src/TA_main2main_workflow/scripts/update_commit_reference.py b/src/TA_main2main_workflow/scripts/update_commit_reference.py deleted file mode 100644 index 5da0b8a..0000000 --- a/src/TA_main2main_workflow/scripts/update_commit_reference.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -"""Update version tracking references after a successful upstream sync. - -For Triton-Ascend (a fork of Triton), update the version tracking file -(version.txt) to record the new upstream commit that was synced. Also -creates a sync metadata file in the workspace for audit trail. - -Output: - - Updated version.txt in triton-ascend repo - - workspace/sync_meta.json with sync details -""" - -from __future__ import annotations - -import json -from datetime import datetime -from pathlib import Path - -from TA_main2main_workflow.utils import ( - WORKSPACE_DIR, run_git, run_git_no_check, - print_section, print_status, print_info, print_key_value, -) - - -def _read_version_file(repo: Path) -> str | None: - """Read the current version.txt if it exists.""" - version_path = repo / "version.txt" - if version_path.exists(): - return version_path.read_text(encoding="utf-8").strip() - return None - - -def _write_version_file(repo: Path, version: str) -> None: - """Write the version.txt file.""" - version_path = repo / "version.txt" - version_path.write_text(version + "\n", encoding="utf-8") - - -def _get_commit_date(repo: Path, commit: str) -> str: - """Get ISO date of a commit.""" - try: - return run_git(repo, "log", "-1", "--format=%cI", commit).strip() - except Exception: - return "" - - -def run_update( - ascend_path: Path, - old_commit: str, - new_commit: str, - work_branch: str = "", -) -> dict: - """Update version tracking after successful upstream sync. - - Args: - ascend_path: Path to the triton-ascend repository - old_commit: Previous upstream commit (merge-base before sync) - new_commit: New upstream commit that was synced - work_branch: Name of the work branch used for the sync - - Returns: - dict with 'files_updated' list and 'sync_meta' - """ - print_section("Update Commit Reference") - - files_updated: list[str] = [] - - old_version = _read_version_file(ascend_path) - short_sha = new_commit[:12] - sync_date = datetime.now().strftime("%Y-%m-%d") - - if old_version: - print_info(f"Current version.txt: {old_version}") - else: - print_info("No version.txt found — creating one") - - new_version = f"upstream-triton-{short_sha}-synced-{sync_date}" - _write_version_file(ascend_path, new_version) - files_updated.append("version.txt") - print_status(True, f"version.txt updated: {new_version}") - - try: - run_git(ascend_path, "add", "version.txt") - except Exception: - pass - - ascend_head = run_git(ascend_path, "rev-parse", "HEAD").strip() - old_commit_date = _get_commit_date(ascend_path, old_commit) - new_commit_date = _get_commit_date(ascend_path, new_commit) - - sync_meta = { - "sync_date": sync_date, - "old_upstream_commit": old_commit, - "new_upstream_commit": new_commit, - "old_commit_date": old_commit_date, - "new_commit_date": new_commit_date, - "triton_ascend_head": ascend_head, - "work_branch": work_branch, - "version_txt": new_version, - } - - meta_path = WORKSPACE_DIR / "sync_meta.json" - meta_path.write_text( - json.dumps(sync_meta, indent=2, ensure_ascii=False) + "\n", - encoding="utf-8", - ) - - print_key_value("Old upstream", f"{old_commit[:12]} ({old_commit_date[:10]})") - print_key_value("New upstream", f"{new_commit[:12]} ({new_commit_date[:10]})") - print_key_value("Ascend HEAD", ascend_head[:12]) - print_key_value("Sync metadata", str(meta_path)) - print_status(True, f"Updated {len(files_updated)} file(s)") - - return { - "files_updated": files_updated, - "sync_meta": sync_meta, - } diff --git a/src/TA_main2main_workflow/utils.py b/src/TA_main2main_workflow/utils.py deleted file mode 100644 index bc9912f..0000000 --- a/src/TA_main2main_workflow/utils.py +++ /dev/null @@ -1,432 +0,0 @@ -"""Shared constants, git helpers, and console output formatting for TA_main2main_workflow.""" - -import os -import shutil -import subprocess -import time -from datetime import datetime -from pathlib import Path -from typing import Any - -# ── Flow routing signals ───────────────────────────────────────────────────── -UpgradeCompleted = "UpgradeCompleted" -UpgradeFailed = "UpgradeFailed" -HasNewCommits = "HasNewCommits" -HasNoNewCommits = "HasNoNewCommits" -MergeSuccess = "MergeSuccess" -MergeConflict = "MergeConflict" -TestsPassed = "TestsPassed" -TestsFailed = "TestsFailed" - -# ── Workspace paths ────────────────────────────────────────────────────────── -_PACKAGE_DIR = Path(__file__).resolve().parent # TA_main2main_workflow package dir -_WORKSPACE_DEFAULT = _PACKAGE_DIR / "workspace" -WORKSPACE_DIR = Path(os.getenv("TA_MAIN2MAIN_WORKSPACE", str(_WORKSPACE_DEFAULT))) -REPOS_DIR_NAME = "repos" -TRITON_REPO_NAME = "triton" -TRITON_ASCEND_REPO_NAME = "triton-ascend" - -# ── Step-planning constants ────────────────────────────────────────────────── -LINE_BUDGET = 1000 -BASE_LINE_BUDGET = 1000 -BASE_COMMIT_COUNT_BUDGET = 5 # deprecated — no longer used as a step limit -# Directories in upstream triton whose changed lines count toward the budget -SOURCE_DIRS = ["python/triton/", "lib/", "include/"] -# File that tracks the LLVM version — commits that modify it get solo steps -LLVM_HASH_FILE = "cmake/llvm-hash.txt" -# Env var to control the line budget at runtime -ENV_LINE_BUDGET = "TA_LINE_BUDGET" -# Env var to control the commit-count budget at runtime -ENV_COMMIT_BUDGET = "TA_COMMIT_BUDGET" -# Env var to enable single-step mode (per-step merge → IR → build → test → fix) -ENV_SINGLE_STEP_MODE = "TA_SINGLE_STEP_MODE" -# Env var to control the base branch for work branches, diffs, and pre-commit. -# Defaults to "main". Set to "master", "develop", etc. to use a different base. -ENV_BASE_BRANCH = "TA_BASE_BRANCH" - - -def get_base_branch_ref(remote: str = "origin") -> str: - """Return the full base-branch ref (e.g. origin/main). - - Controlled by the TA_BASE_BRANCH env var; defaults to 'main'. - """ - branch = os.getenv(ENV_BASE_BRANCH, "main") - return f"{remote}/{branch}" - -# ── Output file names ──────────────────────────────────────────────────────── -DETECT_FILE = "detect.json" -STEPS_FILE = "steps.json" -MERGE_LOG_FILE = "merge.log" -MERGE_RESULT_FILE = "merge_result.json" -BUILD_LOG_FILE = "build.log" -BUILD_RESULT_FILE = "build_result.json" -TEST_RESULT_FILE = "test_result.json" -CONFLICT_LOG_DIR = "conflicts" -FIX_LOG_DIR = "fixes" -STEPS_DIR = "steps" -FINAL_SUMMARY_FILE = "final_summary.md" -FINAL_TARGET_PATCH_FILE = "final_target.patch" -EACH_STEP_SUMMARY_FILE = "step_summary.md" -EACH_STEP_TARGET_PATCH_FILE = "step_target.patch" -PRE_CI_CHECK_FILE = "pre_ci_check.json" -CODE_STRUCTURE_GUIDE_FILE = "code-structure-guide.md" - -# ── IR Analysis & Patch paths ──────────────────────────────────────────────── -IR_ANALYSIS_DIR = "ir-analysis" -IR_PATCHES_DIR = "ir-patches" -IR_OPS_REPORT_FILE = "ops_report.json" -IR_CHANGES_REPORT_FILE = "changes_report.json" -IR_DIAGNOSIS_FILE = "ir_diagnosis.json" - -# ── Per-step LLVM change analysis (single-step mode) ───────────────────────── -LLVM_CHANGE_ANALYSIS_DIR = "llvm_change_analysis" - -# ── IR Patch loop constants ────────────────────────────────────────────────── -IR_MAX_ITERATIONS = 3 - -# ── Timing tracker ─────────────────────────────────────────────────────────── -_phase_timers: dict[str, float] = {} -_flow_start_time: float = 0.0 - - -def commit_count_budget(line_budget: int = LINE_BUDGET) -> int: - """DEPRECATED: Steps are now determined solely by line budget. - - Kept for backward compatibility with existing step plan files. - Returns a derived value from line_budget, but no longer used as a - hard limit during step planning. - """ - import math - import os - base = int(os.getenv(ENV_COMMIT_BUDGET, str(BASE_COMMIT_COUNT_BUDGET))) - return max(1, round(base * math.sqrt(line_budget / BASE_LINE_BUDGET))) - - -def _ts() -> str: - return datetime.now().strftime("%H:%M:%S") - - -# ═══════════════════════════════════════════════════════════════════════════════ -# Console Output Helpers — all progress printed locally, no CrewAI web UI needed -# ═══════════════════════════════════════════════════════════════════════════════ - -def print_header(title: str) -> None: - width = 72 - print(f"\n╔{'═' * width}╗", flush=True) - print(f"║ {title:^{width}} ║", flush=True) - print(f"╚{'═' * width}╝", flush=True) - - -def print_section(title: str) -> None: - print(f"\n{'─' * 60}", flush=True) - print(f" [{_ts()}] {title}", flush=True) - print(f"{'─' * 60}", flush=True) - - -def print_step(step_num: int, total: int, name: str) -> None: - print(f"\n ▸ [{step_num}/{total}] {name} @ {_ts()}", flush=True) - - -def print_status(ok: bool, msg: str) -> None: - icon = "✔" if ok else "✘" - print(f" {icon} {msg}", flush=True) - - -def print_info(msg: str) -> None: - print(f" ℹ {msg}", flush=True) - - -def print_warn(msg: str) -> None: - print(f" ⚠ {msg}", flush=True) - - -def print_error(msg: str) -> None: - print(f" ✘ {msg}", flush=True) - - -def print_key_value(key: str, value: Any) -> None: - print(f" {key}: {value}", flush=True) - - -def print_separator() -> None: - print(f" {'─' * 56}", flush=True) - - -def print_flow_progress(phase: str, detail: str = "") -> None: - msg = f"[{_ts()}] [{phase}] {detail}" if detail else f"[{_ts()}] [{phase}]" - print(msg, flush=True) - - -def start_timer(name: str) -> None: - global _flow_start_time - _phase_timers[name] = time.monotonic() - if not _flow_start_time: - _flow_start_time = time.monotonic() - - -def stop_timer(name: str) -> float: - start = _phase_timers.pop(name, None) - if start is None: - return 0.0 - elapsed = time.monotonic() - start - print(f" ⏱ {name} took {elapsed:.1f}s", flush=True) - return elapsed - - -def print_elapsed_total() -> None: - if _flow_start_time: - total = time.monotonic() - _flow_start_time - print(f"\n ⏱ Total elapsed: {total:.1f}s ({total/60:.1f}m)", flush=True) - - -def print_summary_table(rows: list[tuple[str, str, str]]) -> None: - status_icons = {"PASS": "✔", "FAIL": "✘", "SKIP": "○", "WARN": "⚠"} - print(f"\n{'═' * 72}", flush=True) - print(f" SYNC SUMMARY @ {_ts()}", flush=True) - print(f"{'═' * 72}", flush=True) - print(f" {'Phase':<30} {'Status':<8} {'Details'}", flush=True) - print(f" {'─' * 30} {'─' * 8} {'─' * 32}", flush=True) - for step, status, detail in rows: - icon = status_icons.get(status, "?") - print(f" {step:<30} {icon} {status:<5} {detail}", flush=True) - print(f"{'═' * 72}", flush=True) - - -def print_conflict_list(files: list[str]) -> None: - if not files: - print_info("No conflicts") - return - print(f" Conflicted files ({len(files)}):") - for i, f in enumerate(files, 1): - print(f" {i}. {f}") - - -def print_ai_call_info(backend: str, mode: str, attempt: int, max_attempts: int) -> None: - print(f"\n ╭─ AI Call ─────────────────────────────────────────────", flush=True) - print(f" │ Backend: {backend}", flush=True) - print(f" │ Mode: {mode}", flush=True) - print(f" │ Attempt: {attempt}/{max_attempts}", flush=True) - print(f" │ Time: {_ts()}", flush=True) - print(f" ╰──────────────────────────────────────────────────────", flush=True) - - -def print_ai_result(ok: bool, modified_files: list[str] = (), summary: str = "") -> None: - icon = "✔" if ok else "✘" - print(f"\n ╭─ AI Result ───────────────────────────────────────────", flush=True) - print(f" │ Status: {icon} {'Success' if ok else 'Failed'}", flush=True) - if modified_files: - print(f" │ Modified files ({len(modified_files)}):", flush=True) - for f in modified_files: - print(f" │ • {f}", flush=True) - if summary: - preview = summary[:500] + "..." if len(summary) > 500 else summary - print(f" │ Summary: {preview}", flush=True) - print(f" ╰──────────────────────────────────────────────────────", flush=True) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# Git helpers -# ═══════════════════════════════════════════════════════════════════════════════ - -def run_git(repo: Path | str, *args: str) -> str: - result = subprocess.run( - ["git", *args], - cwd=str(repo), - check=True, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - return result.stdout - - -def run_git_no_check(repo: Path | str, *args: str) -> subprocess.CompletedProcess: - return subprocess.run( - ["git", *args], - cwd=str(repo), - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - - -def is_git_url(path: str) -> bool: - return path.startswith(("https://", "http://", "git@")) - - -def clone_repo(url: str, target: str) -> None: - print(f"[init] Cloning {url} → {target}") - subprocess.run(["git", "clone", url, target], check=True) - - -def resolve_path(raw: str, name: str) -> str: - if is_git_url(raw): - target = WORKSPACE_DIR / REPOS_DIR_NAME / name - if target.exists(): - shutil.rmtree(target) - target.mkdir(parents=True, exist_ok=True) - clone_repo(raw, str(target)) - return str(target) - return raw - - -def get_repo_head(repo: Path) -> str: - if not repo.exists(): - raise FileNotFoundError(f"Repository path does not exist: {repo}") - return run_git(repo, "rev-parse", "HEAD").strip() - - -def get_merge_base(repo: Path, commit_a: str, commit_b: str) -> str: - return run_git(repo, "merge-base", commit_a, commit_b).strip() - - -def has_merge_conflicts(repo: Path) -> bool: - result = run_git_no_check(repo, "diff", "--name-only", "--diff-filter=U") - return bool(result.stdout.strip()) - - -def get_conflict_files(repo: Path) -> list[str]: - result = run_git(repo, "diff", "--name-only", "--diff-filter=U") - return [f for f in result.strip().splitlines() if f] - - -def get_modified_files(repo: Path, base_ref: str = "HEAD") -> list[str]: - result = run_git(repo, "diff", "--name-only", base_ref) - return [f for f in result.strip().splitlines() if f] - - -def get_unstaged_diff(repo: Path) -> str: - return run_git(repo, "diff") - - -def get_staged_diff(repo: Path) -> str: - return run_git(repo, "diff", "--cached") - - -# ═══════════════════════════════════════════════════════════════════════════════ -# Submodule helpers — AscendNPU-IR -# ═══════════════════════════════════════════════════════════════════════════════ - -_ASCENDNPU_IR_SUBMODULE = "third_party/ascend/AscendNPU-IR" -_ASCENDNPU_IR_REMOTE = "https://github.com/TecJesh/AscendNPU-IR.git" -_ASCENDNPU_IR_REMOTE_NAME = "npuir-push" # dedicated remote, never overwrite origin - - -def _submodule_path(repo: Path) -> Path: - """Resolve the AscendNPU-IR submodule path.""" - return repo / _ASCENDNPU_IR_SUBMODULE - - -def submodule_has_changes(repo: Path) -> bool: - """Check if the AscendNPU-IR submodule has uncommitted changes.""" - sm = _submodule_path(repo) - if not sm.exists(): - return False - proc = run_git_no_check(sm, "status", "--porcelain") - return bool(proc.stdout.strip()) - - -def commit_submodule(repo: Path, commit_msg: str) -> bool: - """Commit uncommitted changes inside the AscendNPU-IR submodule. - - Stages ALL changes (including new files) with 'git add -A' and commits - them. Uses -A (not -u) so AI-created files are not silently dropped. - Returns True if a new commit was created. - """ - sm = _submodule_path(repo) - if not sm.exists(): - print_info("[submodule] AscendNPU-IR submodule not found — skipping") - return False - - if not submodule_has_changes(repo): - print_info("[submodule] No uncommitted changes in AscendNPU-IR") - return False - - print_section("Commit AscendNPU-IR Submodule") - try: - run_git(sm, "add", "-A") - # Show what will be committed - staged = run_git(sm, "diff", "--cached", "--name-only").strip() - if staged: - print_info(f"[submodule] Files staged ({len(staged.splitlines())}):") - for f in staged.splitlines()[:10]: - print_info(f" - {f}") - run_git(sm, "commit", "-s", "-m", commit_msg) - new_head = run_git(sm, "rev-parse", "HEAD").strip() - print_status(True, f"Committed AscendNPU-IR: {new_head[:12]}") - return True - except subprocess.CalledProcessError as e: - stderr = (e.stderr or "").strip() if hasattr(e, 'stderr') else str(e) - if "nothing to commit" in stderr.lower(): - print_info("[submodule] Nothing to commit") - return False - print_warn(f"Could not commit AscendNPU-IR submodule: {stderr[-200:]}") - return False - - -def push_submodule( - repo: Path, - branch: str, - remote: str = _ASCENDNPU_IR_REMOTE, - remote_name: str = _ASCENDNPU_IR_REMOTE_NAME, - force: bool = False, -) -> bool: - """Push the AscendNPU-IR submodule branch to its remote. - - Sets up the remote if it doesn't exist, then pushes the given branch. - By default uses force-with-lease for safety; pass force=True for --force. - Returns True on success. - """ - sm = _submodule_path(repo) - if not sm.exists(): - print_warn("[submodule] AscendNPU-IR submodule not found — cannot push") - return False - - print_section("Push AscendNPU-IR Submodule") - - # ── Set up dedicated push remote (never touch origin) ── - # Remove stale npuir-push if it exists, then add fresh - run_git_no_check(sm, "remote", "remove", remote_name) - print_info(f"[submodule] Adding push remote '{remote_name}' → {remote}") - run_git(sm, "remote", "add", remote_name, remote) - - # ── Create branch at current HEAD (don't switch, stay on detached HEAD) ── - current_head = run_git(sm, "rev-parse", "HEAD").strip() - proc = run_git_no_check(sm, "branch", "-f", branch, current_head) - if proc.returncode != 0: - print_error(f"[submodule] Failed to create branch '{branch}': " - f"{proc.stderr.strip()}") - return False - - # ── Configure auth ── - gh_token = os.getenv("GH_TOKEN", "") - if gh_token: - try: - current_url = run_git(sm, "remote", "get-url", remote_name).strip() - if current_url.startswith("https://") and "x-access-token" not in current_url: - clean_url = current_url.replace("https://", "", 1) - if "@" in clean_url: - clean_url = clean_url.split("@", 1)[1] - new_url = f"https://x-access-token:{gh_token}@{clean_url}" - run_git(sm, "remote", "set-url", remote_name, new_url) - safe = f"https://x-access-token:***@{clean_url}" - print_info(f"[submodule] Remote URL rewritten: {safe}") - except Exception as exc: - print_warn(f"[submodule] Could not configure remote auth: {exc}") - - # ── Push ── - try: - push_args = ["push"] - if force: - push_args.append("--force") - else: - push_args.append("--force-with-lease") - push_args.extend([remote_name, branch]) - run_git(sm, *push_args) - print_status(True, f"Pushed AscendNPU-IR branch '{branch}' to {remote_name}") - return True - except Exception as e: - print_error(f"[submodule] Failed to push AscendNPU-IR: {e}") - return False diff --git a/src/TA_main2main_workflow/utils/__init__.py b/src/TA_main2main_workflow/utils/__init__.py new file mode 100644 index 0000000..ba74294 --- /dev/null +++ b/src/TA_main2main_workflow/utils/__init__.py @@ -0,0 +1,54 @@ +"""Utility package for TA_main2main_workflow.""" + +from __future__ import annotations + +import os +from pathlib import Path + +# ═══════════════════════════════════════════════════════════════════════════ +# Workspace paths +# ═══════════════════════════════════════════════════════════════════════════ + +WORKSPACE_DIR = Path(os.getenv("TA_MAIN2MAIN_WORKSPACE", str(Path.cwd() / "workspace"))) + +# ═══════════════════════════════════════════════════════════════════════════ +# Flow routing signals +# ═══════════════════════════════════════════════════════════════════════════ + +UpgradeCompleted = "UpgradeCompleted" +UpgradeFailed = "UpgradeFailed" + +# ═══════════════════════════════════════════════════════════════════════════ +# Step-planning constants +# ═══════════════════════════════════════════════════════════════════════════ + +LLVM_HASH_FILE = "cmake/llvm-hash.txt" +ENV_BASE_BRANCH = "TA_BASE_BRANCH" + + +def get_base_branch_ref(remote: str = "origin") -> str: + branch = os.getenv(ENV_BASE_BRANCH, "upstream_sync") + return f"{remote}/{branch}" + + +# ═══════════════════════════════════════════════════════════════════════════ +# Output file names +# ═══════════════════════════════════════════════════════════════════════════ + +DETECT_FILE = "detect.json" +STEPS_FILE = "steps.json" +BUILD_RESULT_FILE = "build_result.json" +TEST_RESULT_FILE = "test_result.json" +STEPS_DIR = "steps" +FINAL_SUMMARY_FILE = "final_summary.md" +FINAL_TARGET_PATCH_FILE = "final_target.patch" +PRE_CI_CHECK_FILE = "pre_ci_check.json" + +# ═══════════════════════════════════════════════════════════════════════════ +# Re-exports +# ═══════════════════════════════════════════════════════════════════════════ + +from TA_main2main_workflow.utils.config import TAConfig # noqa: F401, E402 +from TA_main2main_workflow.utils.context import WorkflowContext # noqa: F401, E402 +from TA_main2main_workflow.utils.git import run_git, run_git_no_check # noqa: F401, E402 +from TA_main2main_workflow.utils.logging import get_logger, TALogger # noqa: F401, E402 diff --git a/src/TA_main2main_workflow/utils/config.py b/src/TA_main2main_workflow/utils/config.py new file mode 100644 index 0000000..9ba240b --- /dev/null +++ b/src/TA_main2main_workflow/utils/config.py @@ -0,0 +1,118 @@ +"""Configuration for TA_main2main_workflow. + +Only user-configurable parameters. Fixed paths inside triton-ascend +repo are defined where they're used, not here. + +Priority: CLI args > env vars > defaults +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Literal + + +AIBackendChoice = Literal["opencode", "claude", "auto"] + + +@dataclass +class TAConfig: + """User-configurable parameters for a workflow run.""" + + # ── Repository ──────────────────────────────────────────────────────── + triton_ascend_path: str = "" # local path (skip clone if set) + triton_ascend_url: str = "https://github.com/triton-lang/triton-ascend.git" + triton_path: str = "" # local triton checkout (for offline/separate-history) + triton_upstream_url: str = "https://github.com/triton-lang/triton.git" + target_commit: str = "" + + # ── AI Backend ──────────────────────────────────────────────────────── + ai_backend: AIBackendChoice = "auto" + ai_timeout_minutes: int = 30 + ai_stale_seconds: int = 1200 + ai_max_stale_retries: int = 3 + + # ── Retry / Budget ──────────────────────────────────────────────────── + max_retries: int = 10 + line_budget: int = 1000 + + # ── Build / Test parallelism ────────────────────────────────────────── + llvm_install_prefix: str = "" + llvm_repo_url: str = "https://github.com/llvm/llvm-project.git" + build_procs: int = 32 + test_procs: int = 8 + + # ── Skip flags ──────────────────────────────────────────────────────── + resume: bool = False # skip steps whose output already exists + skip_ai_analysis: bool = False + skip_build: bool = False + skip_e2e_test: bool = False + skip_llvm_rebuild: bool = False + + # ── Git / Branch ────────────────────────────────────────────────────── + base_branch: str = "upstream_sync" + progressive_merge: bool = True + + # ── PR / Push ───────────────────────────────────────────────────────── + push_to_github: bool = False + github_repo: str = "triton-lang/triton-ascend" + + # ═══════════════════════════════════════════════════════════════════════ + @classmethod + def from_env(cls) -> TAConfig: + return cls( + triton_ascend_path=os.getenv("TRITON_ASCEND_PATH", ""), + triton_ascend_url=os.getenv( + "TRITON_ASCEND_URL", "https://github.com/triton-lang/triton-ascend.git" + ), + triton_path=os.getenv("TRITON_PATH", ""), + triton_upstream_url=os.getenv( + "TRITON_UPSTREAM_URL", "https://github.com/triton-lang/triton.git" + ), + target_commit=os.getenv("TRITON_TARGET_COMMIT", ""), + ai_backend=_env_choice( + "AI_BACKEND", ["opencode", "claude", "auto"], "auto" + ), + ai_timeout_minutes=_env_int("TA_AI_TIMEOUT_MINUTES", 30), + ai_stale_seconds=_env_int("TA_AI_STALE_SECONDS", 1200), + ai_max_stale_retries=_env_int("TA_AI_MAX_STALE_RETRIES", 3), + max_retries=_env_int("TA_MAX_RETRIES", 10), + line_budget=_env_int("TA_LINE_BUDGET", 1000), + llvm_install_prefix=os.getenv("LLVM_INSTALL_PREFIX", ""), + llvm_repo_url=os.getenv( + "LLVM_REPO_URL", "https://github.com/llvm/llvm-project.git" + ), + build_procs=_env_int("BUILD_PROCS", 32), + test_procs=_env_int("TEST_PROCS", 8), + resume=_env_bool("TA_RESUME", False), + skip_ai_analysis=_env_bool("SKIP_AI_ANALYSIS", False), + skip_build=_env_bool("SKIP_BUILD", False), + skip_e2e_test=_env_bool("SKIP_E2E_TEST", False), + skip_llvm_rebuild=_env_bool("SKIP_LLVM_REBUILD", False), + base_branch=os.getenv("TA_BASE_BRANCH", "upstream_sync"), + progressive_merge=_env_bool("TA_PROGRESSIVE_MERGE", True), + push_to_github=_env_bool("PUSH_TO_GITHUB", False), + github_repo=os.getenv("GITHUB_REPO", "triton-lang/triton-ascend"), + ) + + +def _env_bool(name: str, default: bool) -> bool: + val = os.getenv(name, "").lower() + if val in ("true", "1", "yes"): + return True + if val in ("false", "0", "no"): + return False + return default + + +def _env_int(name: str, default: int) -> int: + try: + return int(os.getenv(name, str(default))) + except (TypeError, ValueError): + return default + + +def _env_choice(name: str, choices: list[str], default: str) -> str: + val = os.getenv(name, default).lower() + return val if val in choices else default diff --git a/src/TA_main2main_workflow/utils/context.py b/src/TA_main2main_workflow/utils/context.py new file mode 100644 index 0000000..52feb92 --- /dev/null +++ b/src/TA_main2main_workflow/utils/context.py @@ -0,0 +1,73 @@ +"""WorkflowContext — shared state carrier between pipeline steps. + +A flat dataclass that each pipeline step reads from and returns an updated +copy of. Steps never mutate the context in place. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field, replace +from pathlib import Path + + +@dataclass +class WorkflowContext: + """All mutable state that flows through the sync pipeline. + + Each step function takes a ``WorkflowContext``, reads what it needs, + and returns a **new** instance with updated fields (via + :meth:`copy_with`). This makes data flow explicit and testable. + """ + + # ── Input configuration (set once at start) ──────────────────────────── + triton_ascend_path: str = "" + + # ── Remote names (set by prepare step) ───────────────────────────────── + origin_remote: str = "origin" + upstream_remote: str = "triton-upstream" + + # ── Git state ────────────────────────────────────────────────────────── + merge_base: str = "" + ascend_head: str = "" + target_commit: str = "" + + # ── Detection results (produced by detect step) ─────────────────────── + upstream_commits: list[dict] = field(default_factory=list) + upstream_commits_count: int = 0 + changed_files_count: int = 0 + changed_lines_total: int = 0 + has_new_commits: bool = False + + # ── Step plan (produced by plan step) ────────────────────────────────── + steps: list[dict] = field(default_factory=list) + total_steps: int = 0 + current_step: int = 0 + + # ── Merge results (produced by merge step) ──────────────────────────── + merge_has_conflicts: bool = False + conflict_files: list[str] = field(default_factory=list) + + # ── Build / test results ────────────────────────────────────────────── + build_passed: bool = False + test_passed: bool = False + fix_errors: list[str] = field(default_factory=list) + + # ── Retry tracking ──────────────────────────────────────────────────── + retry_count: int = 0 + + # ═══════════════════════════════════════════════════════════════════════ + # Helpers + # ═══════════════════════════════════════════════════════════════════════ + + def copy_with(self, **kwargs) -> WorkflowContext: + """Return a new WorkflowContext with the given fields updated. + + Usage:: + + ctx = ctx.copy_with(build_passed=True, retry_count=1) + """ + return replace(self, **kwargs) + + @property + def ascend_path(self) -> Path: + return Path(self.triton_ascend_path) diff --git a/src/TA_main2main_workflow/utils/git.py b/src/TA_main2main_workflow/utils/git.py new file mode 100644 index 0000000..4c69c4a --- /dev/null +++ b/src/TA_main2main_workflow/utils/git.py @@ -0,0 +1,75 @@ +"""Git helpers — thin wrappers around ``subprocess.run`` for git operations. + +``run_git`` automatically retries on network-related failures (fetch, clone, push). +""" + +from __future__ import annotations + +import subprocess +import time +from pathlib import Path + +_RETRIES = 3 +_RETRY_DELAY = 5 # seconds, multiplied by attempt number + +# Operations that auto-retry on any failure (typically network-related) +_RETRY_OPS = {"fetch", "clone", "push", "pull", "remote", "ls-remote"} + + +def run_git(repo: Path | str, *args: str, quiet: bool = False) -> str: + """Run a git command in *repo*, return stdout. Raises on failure. + + Automatically retries fetch/clone/push/pull on any failure. + Set *quiet* to suppress logging (useful for bulk calls like scanning commits). + """ + from TA_main2main_workflow.utils.logging import get_logger + + log = get_logger("git") + if not quiet: + log.info(f"[{Path(repo).name}] $ git {' '.join(args)}") + operation = args[0] if args else "" + + last_error = "" + for attempt in range(1, _RETRIES + 1): + result = subprocess.run( + ["git", *args], + cwd=str(repo), + check=False, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode == 0: + out = result.stdout.strip() + if out and not quiet: + preview = out[:200] + "..." if len(out) > 200 else out + log.info(f" → {preview}") + return result.stdout + + stderr = (result.stderr or "").strip() + if operation in _RETRY_OPS and attempt < _RETRIES: + log.warning(f" retry {attempt}/{_RETRIES}: {stderr[:120]}") + time.sleep(_RETRY_DELAY * attempt) + last_error = stderr + continue + + raise subprocess.CalledProcessError( + result.returncode, ["git", *args], result.stdout, result.stderr + ) + + raise subprocess.CalledProcessError( + -1, ["git", *args], "", f"Failed after {_RETRIES} retries: {last_error}" + ) + + +def run_git_no_check(repo: Path | str, *args: str) -> subprocess.CompletedProcess: + """Run a git command in *repo*, never raise on non-zero exit.""" + return subprocess.run( + ["git", *args], + cwd=str(repo), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) diff --git a/src/TA_main2main_workflow/utils/logging.py b/src/TA_main2main_workflow/utils/logging.py new file mode 100644 index 0000000..8c9a904 --- /dev/null +++ b/src/TA_main2main_workflow/utils/logging.py @@ -0,0 +1,140 @@ +"""Logging setup for TA_main2main_workflow. + +Uses Python's standard ``logging`` module with a custom formatter that +preserves the visual style of the old ``console.py`` (headers, sections, +status icons) while routing everything through the logging framework. + +Usage:: + + from TA_main2main_workflow.utils.logging import get_logger + log = get_logger(__name__) + log.info("Starting sync...") + log.header("Phase 1: Detect") # boxed header + log.section("Build Triton-Ascend") # section divider + log.step(1, 3, "AI fix") # step indicator + log.status(True, "Build passed") # ✔ / ✘ + log.key_value("target", "abc123") # key: value + log.table(rows) # summary table +""" + +from __future__ import annotations + +import logging +import sys +from datetime import datetime +from typing import Any + + +# ═══════════════════════════════════════════════════════════════════════════ +# Custom logger class +# ═══════════════════════════════════════════════════════════════════════════ + + +class TALogger(logging.getLoggerClass()): + """Logger with extra formatting methods for workflow output.""" + + def header(self, title: str) -> None: + width = 72 + self.info(f"\n╔{'═' * width}╗") + self.info(f"║ {title:^{width}} ║") + self.info(f"╚{'═' * width}╝") + + def section(self, title: str) -> None: + ts = datetime.now().strftime("%H:%M:%S") + self.info(f"\n{'─' * 60}") + self.info(f" [{ts}] {title}") + self.info(f"{'─' * 60}") + + def step(self, num: int, total: int, name: str) -> None: + ts = datetime.now().strftime("%H:%M:%S") + self.info(f"\n ▸ [{num}/{total}] {name} @ {ts}") + + def status(self, ok: bool, msg: str) -> None: + icon = "✔" if ok else "✘" + self.info(f" {icon} {msg}") + + def warn(self, msg: str, *args, **kwargs) -> None: + # Override to use consistent prefix + super().warning(f" ⚠ {msg}", *args, **kwargs) + + def error(self, msg: str, *args, **kwargs) -> None: + super().error(f" ✘ {msg}", *args, **kwargs) + + def key_value(self, key: str, value: Any) -> None: + self.info(f" {key}: {value}") + + def flow_progress(self, phase: str, detail: str = "") -> None: + ts = datetime.now().strftime("%H:%M:%S") + msg = f"[{ts}] [{phase}] {detail}" if detail else f"[{ts}] [{phase}]" + self.info(msg) + + def conflict_list(self, files: list[str]) -> None: + if not files: + self.info(" ℹ No conflicts") + return + self.info(f" Conflicted files ({len(files)}):") + for i, f in enumerate(files, 1): + self.info(f" {i}. {f}") + + def ai_call(self, backend: str, mode: str, attempt: int, max_attempts: int) -> None: + ts = datetime.now().strftime("%H:%M:%S") + self.info(f"\n ╭─ AI Call ─────────────────────────────────────────────") + self.info(f" │ Backend: {backend}") + self.info(f" │ Mode: {mode}") + self.info(f" │ Attempt: {attempt}/{max_attempts}") + self.info(f" │ Time: {ts}") + self.info(f" ╰──────────────────────────────────────────────────────") + + def ai_result( + self, ok: bool, modified_files: list[str] = (), summary: str = "" + ) -> None: + icon = "✔" if ok else "✘" + self.info(f"\n ╭─ AI Result ───────────────────────────────────────────") + self.info(f" │ Status: {icon} {'Success' if ok else 'Failed'}") + if modified_files: + self.info(f" │ Modified files ({len(modified_files)}):") + for f in modified_files: + self.info(f" │ • {f}") + if summary: + preview = summary[:500] + "..." if len(summary) > 500 else summary + self.info(f" │ Summary: {preview}") + self.info(f" ╰──────────────────────────────────────────────────────") + + def table(self, rows: list[tuple[str, str, str]]) -> None: + ts = datetime.now().strftime("%H:%M:%S") + status_icons = {"PASS": "✔", "FAIL": "✘", "SKIP": "○", "WARN": "⚠"} + self.info(f"\n{'═' * 72}") + self.info(f" SYNC SUMMARY @ {ts}") + self.info(f"{'═' * 72}") + self.info(f" {'Phase':<30} {'Status':<8} {'Details'}") + self.info(f" {'─' * 30} {'─' * 8} {'─' * 32}") + for step, status, detail in rows: + icon = status_icons.get(status, "?") + self.info(f" {step:<30} {icon} {status:<5} {detail}") + self.info(f"{'═' * 72}") + + def elapsed(self, seconds: float) -> None: + self.info(f"\n ⏱ Total elapsed: {seconds:.1f}s ({seconds / 60:.1f}m)") + + +# ═══════════════════════════════════════════════════════════════════════════ +# Setup +# ═══════════════════════════════════════════════════════════════════════════ + +logging.setLoggerClass(TALogger) + + +def get_logger(name: str) -> TALogger: + """Return a configured TALogger for *name*.""" + log = logging.getLogger(name) + if not log.handlers: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter("%(message)s")) + log.addHandler(handler) + log.setLevel(logging.INFO) + log.propagate = False + return log # type: ignore[return-value] + + +# Default logger for simple imports +default_logger = get_logger("ta-workflow") diff --git a/src/TA_main2main_workflow/utils/submodule.py b/src/TA_main2main_workflow/utils/submodule.py new file mode 100644 index 0000000..098fdbb --- /dev/null +++ b/src/TA_main2main_workflow/utils/submodule.py @@ -0,0 +1,97 @@ +"""AscendNPU-IR submodule helpers.""" + +from __future__ import annotations +import os, subprocess +from pathlib import Path +from TA_main2main_workflow.utils.logging import get_logger +from TA_main2main_workflow.utils.git import run_git, run_git_no_check + +log = get_logger(__name__) +_ASCENDNPU_IR_SUBMODULE = "third_party/ascend/AscendNPU-IR" +_ASCENDNPU_IR_REMOTE = "https://github.com/TecJesh/AscendNPU-IR.git" +_ASCENDNPU_IR_REMOTE_NAME = "npuir-push" + + +def _submodule_path(repo: Path) -> Path: + return repo / _ASCENDNPU_IR_SUBMODULE + + +def submodule_has_changes(repo: Path) -> bool: + sm = _submodule_path(repo) + if not sm.exists(): + return False + return bool(run_git_no_check(sm, "status", "--porcelain").stdout.strip()) + + +def commit_submodule(repo: Path, commit_msg: str) -> bool: + sm = _submodule_path(repo) + if not sm.exists(): + return False + if not submodule_has_changes(repo): + log.info("[submodule] No changes") + return False + log.section("Commit AscendNPU-IR Submodule") + try: + run_git(sm, "add", "-A") + run_git(sm, "commit", "-s", "-m", commit_msg) + log.status( + True, + f"Committed AscendNPU-IR: {run_git(sm, 'rev-parse', 'HEAD').strip()[:12]}", + ) + return True + except subprocess.CalledProcessError as e: + if "nothing to commit" in str(getattr(e, "stderr", "")).lower(): + log.info("[submodule] Nothing to commit") + return False + log.warning(f"Could not commit submodule: {e}") + return False + + +def push_submodule( + repo: Path, + branch: str, + remote: str = _ASCENDNPU_IR_REMOTE, + remote_name: str = _ASCENDNPU_IR_REMOTE_NAME, + force: bool = False, +) -> bool: + sm = _submodule_path(repo) + if not sm.exists(): + log.warning("[submodule] Not found") + return False + log.section("Push AscendNPU-IR Submodule") + run_git_no_check(sm, "remote", "remove", remote_name) + run_git(sm, "remote", "add", remote_name, remote) + head = run_git(sm, "rev-parse", "HEAD").strip() + run_git_no_check(sm, "branch", "-f", branch, head) + gh_token = os.getenv("GH_TOKEN", "") + if gh_token: + try: + url = run_git(sm, "remote", "get-url", remote_name).strip() + if url.startswith("https://") and "x-access-token" not in url: + clean = ( + url.replace("https://", "", 1).split("@", 1)[-1] + if "@" in url + else url.replace("https://", "", 1) + ) + run_git( + sm, + "remote", + "set-url", + remote_name, + f"https://x-access-token:{gh_token}@{clean}", + ) + except Exception: + pass + try: + run_git( + sm, + "push", + "--force-with-lease" if not force else "--force", + remote_name, + branch, + ) + log.status(True, f"Pushed AscendNPU-IR branch '{branch}'") + return True + except Exception as e: + log.error(f"Failed to push AscendNPU-IR: {e}") + return False diff --git a/src/TA_main2main_workflow/utils/tracker.py b/src/TA_main2main_workflow/utils/tracker.py new file mode 100644 index 0000000..a4424ee --- /dev/null +++ b/src/TA_main2main_workflow/utils/tracker.py @@ -0,0 +1,49 @@ +"""Execution timer — tracks phase-level and total elapsed time.""" + +from __future__ import annotations + +import time +from contextlib import contextmanager + +# Module-level state +_flow_start_time: float = 0.0 + + +@contextmanager +def timed(name: str): + """Context manager: record elapsed time for a named phase. + + Usage:: + + with timed("merge"): + ctx = merge_upstream_commit(ctx, config) + """ + global _flow_start_time + if not _flow_start_time: + _flow_start_time = time.monotonic() + start = time.monotonic() + try: + yield + finally: + elapsed = time.monotonic() - start + from TA_main2main_workflow.utils.logging import get_logger + + get_logger(__name__).info(f"⏱ {name} took {elapsed:.1f}s") + + +def total_elapsed() -> float: + """Return total seconds since the first timed() call.""" + if _flow_start_time: + return time.monotonic() - _flow_start_time + return 0.0 + + +# Backward compat +def start_timer(name: str) -> None: + """Deprecated: use ``with timed(name):`` instead.""" + pass + + +def stop_timer(name: str) -> float: + """Deprecated: use ``with timed(name):`` instead.""" + return 0.0