Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4c077b4
[Frontend] Add the Triton codegen route (Inductor triton + tnpu passes)
YWHyuk Jul 27, 2026
5686735
[Build] Target torch 2.10 so the frontend and triton-npu agree on tri…
YWHyuk Jul 27, 2026
ef76c24
[CI] Add the triton-npu toolchain image and its workflow
YWHyuk Jul 27, 2026
2173834
[CI] Source triton-npu from PSAL-POSTECH and drop the local workarounds
YWHyuk Jul 27, 2026
e4af91b
[CI] Note only the token as a prerequisite
YWHyuk Jul 27, 2026
5f632d6
[CI] Bump the triton-npu pin to de2c767
YWHyuk Jul 27, 2026
8faa1e5
[CI] Bump the triton-npu pin to e113819
YWHyuk Jul 27, 2026
b442aa0
[CI] Bump the triton-npu pin to fd423b0
YWHyuk Jul 27, 2026
600a6a9
[Frontend] Rename print_operation to visit_operation
YWHyuk Jul 27, 2026
c0df618
[Frontend] Give the trace pipeline a kernel whose body is one work-item
YWHyuk Jul 27, 2026
15ce2a1
[Frontend] Run the Triton route's kernel through TOGSim
YWHyuk Jul 27, 2026
3c82969
[Frontend] Measure the Triton route's tile cost with gem5
YWHyuk Jul 27, 2026
85c4e66
[Frontend] Let the route ask for a multi-axis grid
YWHyuk Jul 27, 2026
9657d38
[Frontend] Take the grid at run time, so one trace serves every shape
YWHyuk Jul 27, 2026
8da4bbc
[Frontend] Pin only the blocks the kernel takes as parameters
YWHyuk Jul 27, 2026
cdd53c0
[Frontend] Pass the kernel's scalar arguments to the tnpu wrapper
YWHyuk Jul 27, 2026
5e77a5f
[Frontend] Run the Triton route's kernel on the launch's own tensors
YWHyuk Jul 27, 2026
68a6aae
[CI] Bump the triton-npu pin to 22df065
YWHyuk Jul 27, 2026
5f52306
[Docs] Report the Triton route against the MLIR one
YWHyuk Jul 28, 2026
05e4edc
[Docs] State where the port ends and the lowering pass begins
YWHyuk Jul 28, 2026
6670e5f
[Docs] Make the next step coverage parity, measured rather than guessed
YWHyuk Jul 28, 2026
c099add
[Docs] Rewrite the report as one piece
YWHyuk Jul 28, 2026
e3672a8
[Docs] Say lowering pass in prose, keep tnpu only where it is a real …
YWHyuk Jul 28, 2026
83bce51
[Docs] Call the lowering layer the NPU lowering pass
YWHyuk Jul 28, 2026
6ddab17
[Docs] Show the grid being rebuilt as the outer loop
YWHyuk Jul 28, 2026
f8f3d0a
[Docs] Make model coverage the goal, op coverage the step to it
YWHyuk Jul 28, 2026
ebbd113
[Docs] Keep the remaining work high level
YWHyuk Jul 28, 2026
31baf29
[CI] Bump the triton-npu pin to 4835b38, on the released spike
YWHyuk Aug 3, 2026
d37d471
[CI] Bump the triton-npu pin to 5d84caf
YWHyuk Aug 3, 2026
65ada99
[CI] Set the spike override in the image, not in each job
YWHyuk Aug 3, 2026
f8e744d
[CI] Run the whole test suite on the Triton route, and report what fails
YWHyuk Aug 3, 2026
d356e35
[Frontend] Say why a kernel was rejected, and run the sweep in parallel
YWHyuk Aug 3, 2026
8e17519
[Docs] Record the first Triton route coverage measurement
YWHyuk Aug 3, 2026
6e3bd7e
[Docs] Back the coverage report with the artifacts it came from
YWHyuk Aug 3, 2026
33a222a
[Docs] Add the Korean coverage report
YWHyuk Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions .github/workflows/triton_npu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
name: Triton codegen route (triton-npu)

# Exercises the Triton codegen route: Inductor's own Triton backend produces the
# kernel and triton-npu lowers it to a RISC-V ELF.
# (PyTorchSimFrontend/triton_backend/README.md)
#
# Separate from the main CI on purpose. The route is WIP, and its toolchain layer
# is ~1.8 GiB that no other job needs, so it neither gates PRs nor slows them
# down. Promote the jobs into pytorchsim_test.yml once the route runs end to end.
#
# Needs secrets.TNPU_TOKEN: a PAT that can read PSAL-POSTECH/triton-npu and its
# toolchain-llvm23 release. The repo is private and the default Actions token is
# scoped to this repository. preflight checks it before the docker build.

on:
pull_request:
branches: [ "master", "develop" ]
paths:
- 'PyTorchSimFrontend/triton_backend/**'
- 'thirdparty/triton-npu.json'
- 'Dockerfile.tnpu'
- 'scripts/ci/tnpu_base_pin.sh'
- '.github/workflows/triton_npu.yml'
workflow_dispatch:

env:
BASE_IMAGE_REPO: ghcr.io/psal-postech/torchsim_base
TNPU_IMAGE_REPO: ghcr.io/psal-postech/torchsim_tnpu_base
APP_IMAGE_REPO: ghcr.io/psal-postech/torchsim_tnpu
SOURCE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}

jobs:
preflight:
name: Check tnpu access
runs-on: [self-hosted, slurm, x86_64]
outputs:
ready: ${{ steps.check.outputs.ready }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.SOURCE_SHA }}
persist-credentials: false

- name: Token and release present
id: check
env:
TNPU_TOKEN: ${{ secrets.TNPU_TOKEN }}
run: |
if [ -z "${TNPU_TOKEN}" ]; then
echo "::error::secrets.TNPU_TOKEN is not set. PSAL-POSTECH/triton-npu is private and the default Actions token cannot read it."
echo "ready=false" >> "$GITHUB_OUTPUT"; exit 1
fi
REPO=$(jq -r '.triton_npu.repository' thirdparty/triton-npu.json)
TAG=$(jq -r '.triton_npu.release_tag' thirdparty/triton-npu.json)
if ! curl -fsS -H "Authorization: Bearer ${TNPU_TOKEN}" \
"https://api.github.com/repos/${REPO}" -o /dev/null; then
echo "::error::TNPU_TOKEN cannot read ${REPO}."
echo "ready=false" >> "$GITHUB_OUTPUT"; exit 1
fi
if ! curl -fsS -H "Authorization: Bearer ${TNPU_TOKEN}" \
"https://api.github.com/repos/${REPO}/releases/tags/${TAG}" -o /dev/null; then
echo "::error::${REPO} has no release tagged '${TAG}'. Mirror the toolchain assets there (see thirdparty/triton-npu.json)."
echo "ready=false" >> "$GITHUB_OUTPUT"; exit 1
fi
echo "ready=true" >> "$GITHUB_OUTPUT"

ensure-tnpu-base:
name: Build tnpu toolchain image
needs: preflight
runs-on: [self-hosted, slurm, big, x86_64]
outputs:
tnpu_image: ${{ steps.pin.outputs.tnpu_image }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.SOURCE_SHA }}
submodules: recursive
persist-credentials: false

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Pins
id: pin
run: |
BASE_PIN="$(bash scripts/ci/thirdparty_base_pin.sh)"
TNPU_PIN="$(bash scripts/ci/tnpu_base_pin.sh)"
echo "BASE_IMAGE=${BASE_IMAGE_REPO}:thirdparty-${BASE_PIN}" >> "$GITHUB_ENV"
# The tnpu layer sits on a specific base, so its tag carries both pins.
echo "TNPU_IMAGE=${TNPU_IMAGE_REPO}:tnpu-${TNPU_PIN}-base-${BASE_PIN}" >> "$GITHUB_ENV"
echo "tnpu_image=${TNPU_IMAGE_REPO}:tnpu-${TNPU_PIN}-base-${BASE_PIN}" >> "$GITHUB_OUTPUT"
echo "TNPU_REF=$(jq -r '.triton_npu.ref' thirdparty/triton-npu.json)" >> "$GITHUB_ENV"

- name: Check tnpu image exists
id: exists
run: |
if docker manifest inspect "${TNPU_IMAGE}" > /dev/null 2>&1; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
fi

- name: Build and push tnpu toolchain image
if: steps.exists.outputs.ok != 'true'
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.tnpu
push: true
build-args: |
BASE_IMAGE=${{ env.BASE_IMAGE }}
TNPU_REF=${{ env.TNPU_REF }}
secrets: |
tnpu_token=${{ secrets.TNPU_TOKEN }}
tags: ${{ env.TNPU_IMAGE }}

build-app:
name: Build app image on tnpu base
needs: ensure-tnpu-base
runs-on: [self-hosted, slurm, big, x86_64]
outputs:
app_image: ${{ steps.name.outputs.app_image }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.SOURCE_SHA }}
submodules: recursive
persist-credentials: false

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image name
id: name
run: echo "app_image=${APP_IMAGE_REPO}:${SOURCE_SHA}" >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
build-args: |
BASE_IMAGE=${{ needs.ensure-tnpu-base.outputs.tnpu_image }}
tags: ${{ steps.name.outputs.app_image }}

tnpu-baselines:
name: triton-npu baselines
needs: build-app
runs-on: [self-hosted, slurm, x86_64]
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# The harness's own kernels, end to end through Spike. This is the gate on
# the toolchain itself: if these regress, nothing downstream is meaningful.
# gemm/bmm need TNPU_VCIX_MATMUL=1 to reach the systolic array.
- name: doctor + add / mul / relu / gemm / bmm
run: |
docker run --rm -e TNPU_VCIX_MATMUL=1 \
${{ needs.build-app.outputs.app_image }} bash -lc '
cd /workspace/triton-npu &&
python3 run.py doctor &&
for k in add mul relu gemm bmm; do
echo "=== $k ===" && python3 run.py kernels/$k.py || exit 1
done'

triton-route:
name: Inductor Triton route
needs: build-app
runs-on: [self-hosted, slurm, x86_64]
# WIP: the launch is deliberately unimplemented, so this reports how far the
# route gets rather than gating. Drop this once the launch lands.
continue-on-error: true
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: test_triton_codegen.py
run: |
docker run --rm -e TORCHSIM_TRITON_CODEGEN=1 \
${{ needs.build-app.outputs.app_image }} \
python3 PyTorchSim/tests/system/test_triton_codegen.py

triton-route-suite:
name: Test suite on the Triton route
needs: build-app
runs-on: [self-hosted, slurm, big, x86_64]
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Gates on the tests that pass today; coverage cannot silently shrink.
- name: Allowlisted tests
run: |
docker run --rm \
${{ needs.build-app.outputs.app_image }} \
python3 PyTorchSim/scripts/ci/triton_route_sweep.py

# Reports the rest. Each failure leaves its kernel and stage IR behind.
- name: Full sweep (report)
continue-on-error: true
run: |
mkdir -p sweep && chmod 777 sweep
docker run --rm -v "$PWD/sweep:/sweep" \
${{ needs.build-app.outputs.app_image }} \
python3 PyTorchSim/scripts/ci/triton_route_sweep.py --all \
--timeout 900 --json /sweep/results.json \
--markdown /sweep/coverage.md --artifacts /sweep/failures
cat sweep/coverage.md >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@v4
if: always()
with:
name: triton-route-coverage
path: sweep/
if-no-files-found: warn

mlir-route-regression:
name: MLIR route still passes
needs: build-app
runs-on: [self-hosted, slurm, x86_64]
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# The tnpu layer adds a second LLVM and a second triton to the image. This
# is the check that it did not disturb the production path.
- name: test_add.py
run: |
docker run --rm \
${{ needs.build-app.outputs.app_image }} \
python3 PyTorchSim/tests/ops/elementwise/test_add.py
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Run a model from `tests/models/Llama/`, `tests/models/DeepSeek/`, etc. similarly

**CI coverage:** the GitHub Actions workflow `.github/workflows/pytorchsim_test.yml` runs an **explicit allowlist** of `tests/*.py` files (~40 jobs, one Docker container per test). Adding a new file under `tests/` does *not* automatically gate PRs — register it in `pytorchsim_test.yml` if you want CI to exercise it. Conversely, files like `tests/ops/attention/test_gqa.py`, `tests/ops/attention/test_gqa_decode.py`, and `tests/system/test_eager.py` exist in the repo but are *not* in CI, so local validation is the only safety net for them.

The Triton codegen route has its own workflow, `.github/workflows/triton_npu.yml`, kept separate because its toolchain layer is ~1.8 GiB that no other job needs. It builds `torchsim_tnpu_base` (pinned by `thirdparty/triton-npu.json` + `Dockerfile.tnpu`) and needs `secrets.TNPU_TOKEN` plus a toolchain release on the private `PSAL-POSTECH/triton-npu`; see `PyTorchSimFrontend/triton_backend/README.md`.

**For fast iteration** (skip functional check):
```bash
export pytorchsim_functional_mode=False # skips Spike
Expand Down Expand Up @@ -137,7 +139,7 @@ Conan deps for TOGSim: `boost/1.79.0`, `robin-hood-hashing/3.11.5`, `spdlog/1.11

## Gotchas / things I've already learned

- The repo expects `python` to be a Python 3.10+ binary with `torch==2.8.0`. The frontend extends the PyTorch 2 Inductor stack — pin to this version.
- The repo expects `python` to be a Python 3.10+ binary with `torch==2.10.0` (torchvision `0.25.0`, triton `3.6.0`). The frontend extends the PyTorch 2 Inductor stack — pin to this version. 2.10 specifically: it is the first release whose Inductor targets triton 3.6, the version triton-npu is built against. The pins live in `Dockerfile.base`, and editing that file changes the base-image tag automatically (the tag is `thirdparty-<sha256 of thirdparty/github-releases.json + Dockerfile.base>`, see `scripts/ci/thirdparty_base_pin.sh`).
- The default Gem5 path is hard-coded to `/workspace/gem5/build/RISCV/gem5.opt`. Override with `GEM5_PATH` if you build elsewhere.
- `_C.cpython-311-*.so` and `torch_openreg/lib/` are build artifacts — already in `.gitignore`, don't commit.
- TOGSim creates a per-PID FIFO under `/tmp/togsim_fifo_<pid>` for command/event comm; if a previous run crashed and left stale FIFOs, they get cleaned up on the next start, but watch for orphaned processes if you Ctrl-C mid-run.
Expand Down
14 changes: 13 additions & 1 deletion Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,20 @@ RUN apt-get -y update && \
rm -rf /var/lib/apt/lists/*

# CPU PyTorch (no CUDA wheels). torchvision is required by the vision model tests.
# torch 2.10 is pinned for the Triton codegen route: it is the first release whose
# Inductor targets triton 3.6, which is the version triton-npu is built against
# (triton 3.6 pins LLVM 23, and both sides of triton-npu's textual IR seam must be
# the same LLVM). On 2.8 the frontend had to be shimmed onto a triton it did not
# expect; on 2.10 the versions simply agree.
RUN python3.11 -m pip install --no-cache-dir \
torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cpu
torch==2.10.0 torchvision==0.25.0 --index-url https://download.pytorch.org/whl/cpu

# Triton, for the Inductor Triton codegen route (PyTorchSimFrontend/triton_backend).
# Inductor imports triton while GENERATING a kernel, so it is needed even though
# nothing here compiles or launches through triton's own runtime -- triton-npu
# compiles the kernel ahead of time to a RISC-V ELF using its own triton build.
# Not a dependency of the CPU torch wheels, hence installed explicitly.
RUN python3.11 -m pip install --no-cache-dir triton==3.6.0

# TorchSim Python dependencies (numpy pinned <2 for transformers/diffusers compat).
RUN python3.11 -m pip install --no-cache-dir \
Expand Down
42 changes: 42 additions & 0 deletions Dockerfile.tnpu
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1.4
#
# triton-npu toolchain layer, for the Triton codegen route only.
# Separate from torchsim_base because it is ~1.8 GiB no other job needs.
# The app image for this route is ./Dockerfile with BASE_IMAGE pointed here.
#
# The repo is private, so the clone and the release downloads both need a token.
# It is a BuildKit secret, not a build-arg: build-args land in the image history.

ARG BASE_IMAGE=ghcr.io/psal-postech/torchsim_base:latest
FROM ${BASE_IMAGE}

ARG TNPU_REPO=PSAL-POSTECH/triton-npu
ARG TNPU_REF=main

WORKDIR /workspace

# Not under $TORCHSIM_DIR: ./Dockerfile copies the PyTorchSim checkout over that
# path afterwards. extension_config reads TNPU_DIR, set below.
RUN --mount=type=secret,id=tnpu_token \
TOKEN="$(cat /run/secrets/tnpu_token)" && \
git clone "https://x-access-token:${TOKEN}@github.com/${TNPU_REPO}.git" \
/workspace/triton-npu && \
git -C /workspace/triton-npu checkout -q "${TNPU_REF}" && \
git -C /workspace/triton-npu remote set-url origin \
"https://github.com/${TNPU_REPO}.git"

# restore.sh owns every pin (setup/versions.env) and unpacks LLVM 23, spike and
# the triton runtime into /workspace.
RUN --mount=type=secret,id=tnpu_token \
GITHUB_TOKEN="$(cat /run/secrets/tnpu_token)" \
/workspace/triton-npu/setup/restore.sh --prebuilt

ENV TNPU_DIR=/workspace/triton-npu
# tnpu defaults to a separate fp8 spike and asks for zvfp8; the released spike
# has neither, and an unknown extension stops spike at startup. Drop once
# PSAL-POSTECH/riscv-isa-sim#7 is in the release.
ENV TNPU_SPIKE=/workspace/riscv-isa-sim/install/bin/spike
ENV TNPU_SPIKE_ISA=rv64gcv_zfh

# Fail the build, not the first CI job.
RUN python3 /workspace/triton-npu/run.py doctor
29 changes: 23 additions & 6 deletions PyTorchSimDevice/torch_openreg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,30 @@

sys.path.append(os.environ.get('TORCHSIM_DIR', default='/workspace/PyTorchSim'))
import PyTorchSimFrontend.extension_config # noqa: F401
from PyTorchSimFrontend import extension_config as _extension_config
from PyTorchSimFrontend.mlir.mlir_codegen_backend import ExtensionWrapperCodegen
from PyTorchSimFrontend.mlir.mlir_scheduling import MLIRScheduling
torch._inductor.codegen.common.register_backend_for_device(
"npu",
lambda scheduling: MLIRScheduling(scheduling),
ExtensionWrapperCodegen
)

# Two mutually exclusive codegen routes for `npu`, chosen here because Inductor
# registers a backend per device, once.
# MLIR (default) hand-written MLIR emission, PyTorchSimFrontend/mlir
# Triton (opt-in) Inductor's own Triton codegen + the triton-npu passes,
# TORCHSIM_TRITON_CODEGEN=1. WIP; see
# PyTorchSimFrontend/triton_backend/README.md
if _extension_config.CONFIG_TRITON_CODEGEN:
from PyTorchSimFrontend.triton_backend import (
TritonNPUScheduling, TritonNPUWrapperCodegen)
torch._inductor.codegen.common.register_backend_for_device(
"npu",
lambda scheduling: TritonNPUScheduling(scheduling),
TritonNPUWrapperCodegen
)
else:
from PyTorchSimFrontend.mlir.mlir_scheduling import MLIRScheduling
torch._inductor.codegen.common.register_backend_for_device(
"npu",
lambda scheduling: MLIRScheduling(scheduling),
ExtensionWrapperCodegen
)

torch_openreg.openreg.init()
sys.modules['torch.npu'] = torch_openreg.openreg
Expand Down
15 changes: 15 additions & 0 deletions PyTorchSimFrontend/extension_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@
CONFIG_TORCHSIM_DUMP_MLIR_IR = int(os.environ.get("TORCHSIM_DUMP_MLIR_IR", default=False))
CONFIG_TORCHSIM_DUMP_LLVM_IR = int(os.environ.get("TORCHSIM_DUMP_LLVM_IR", default=False))

# --- Triton codegen route (WIP, opt-in) --------------------------------------
# Replaces the hand-written MLIR emission in PyTorchSimFrontend/mlir with
# Inductor's own Triton codegen, lowered to the NPU by the triton-npu (tnpu)
# pass pipeline. OFF by default: the MLIR route stays the production path until
# this one is complete. See PyTorchSimFrontend/triton_backend/README.md.
CONFIG_TRITON_CODEGEN = bool(int(os.environ.get("TORCHSIM_TRITON_CODEGEN", default=0)))
# The triton-npu checkout that owns stages 1-5 (ttir -> ttshared -> tnpu passes
# -> RISC-V ELF). It is a SEPARATE repository, deliberately not vendored.
CONFIG_TNPU_DIR = os.environ.get(
"TNPU_DIR", default=os.path.join(CONFIG_TORCHSIM_DIR, "triton-npu"))
# tnpu runs in its own process: its passes need LLVM 23's MLIR bindings while
# this process holds LLVM 20's, and `mlir` is a namespace package, so the two
# cannot coexist in one interpreter (tnpu/config.py:activate_bindings).
CONFIG_TNPU_PYTHON = os.environ.get("TNPU_PYTHON", default=sys.executable)


def get_dump_path():
"""Resolve TORCHSIM_DUMP_PATH and re-point Inductor's cache dir at it.
Expand Down
Loading
Loading