From e3a04d98b136b2d6476e09feccafda3f6b155450 Mon Sep 17 00:00:00 2001 From: mirkodevita Date: Tue, 12 May 2026 15:50:18 +0200 Subject: [PATCH] updated 140tflops builder and added 512 working one with preload at 4. Included also pottisibility to remove barriers during compilation --- .github/workflows/ci.yml | 6 +- docker/Dockerfile | 8 +- .../aot/flash_attention/140tflops/README.md | 82 +++ .../aot/flash_attention/140tflops/caller.cpp | 2 +- .../aot/flash_attention/140tflops/compile.sh | 43 +- .../140tflops/compile_common.sh | 56 ++ .../140tflops/compile_tile512.sh | 56 ++ .../140tflops/fa_dsl_builder.py | 79 +-- .../140tflops/fa_dsl_builder_tile512.py | 658 ++++++++++++++++++ .../140tflops/naive_tpush_dsl_plot.png | Bin 0 -> 104736 bytes examples/aot/flash_attention/140tflops/run.py | 224 +++++- ptodsl/api/pto.py | 8 + ptodsl/api/pto_general.py | 72 +- ptodsl/compiler/ir.py | 4 +- 14 files changed, 1179 insertions(+), 119 deletions(-) create mode 100644 examples/aot/flash_attention/140tflops/README.md create mode 100644 examples/aot/flash_attention/140tflops/compile_common.sh create mode 100755 examples/aot/flash_attention/140tflops/compile_tile512.sh create mode 100644 examples/aot/flash_attention/140tflops/fa_dsl_builder_tile512.py create mode 100644 examples/aot/flash_attention/140tflops/naive_tpush_dsl_plot.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d578d8b..041288ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,10 @@ jobs: env: RELEASE_REPO: hw-native-sys/PTOAS - RELEASE_VER: 0.31 - RELEASE_TAG: v0.31 + RELEASE_VER: 0.37 + RELEASE_TAG: v0.37 CLI_DIR: /installers/ptoas-cli - PTOISA_COMMIT: 0af942568a4f2868673da0a35b0f5b64f27a20d5 + PTOISA_COMMIT: 933ad5d84c98377ca19f1de2e6616ba79136056a steps: - name: Install system packages diff --git a/docker/Dockerfile b/docker/Dockerfile index dc27a0ef..882f5082 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,8 +15,8 @@ RUN pip install --no-cache-dir \ pytest pybind11 nanobind setuptools wheel \ ipython jupyterlab matplotlib pandas -# This specific commit might not be found as it has been forced push over -ARG PTOISA_COMMIT=4e27a104f948e883e0bef44670252381bff794c5 +# This commit is dated 2024-05-12 +ARG PTOISA_COMMIT=933ad5d84c98377ca19f1de2e6616ba79136056a WORKDIR /sources RUN git clone --single-branch --branch master https://gitcode.com/cann/pto-isa.git \ && cd pto-isa && git checkout $PTOISA_COMMIT @@ -29,8 +29,8 @@ ARG CACHE_BURST=1 # ARG ARCH=x86_64 ARG ARCH=aarch64 ARG RELEASE_REPO=hw-native-sys/PTOAS -ARG RELEASE_VER=0.36 -ARG RELEASE_TAG=v${RELEASE_VER} +ARG RELEASE_VER=0.37 +ARG RELEASE_TAG=v0.37 ARG WHEEL_NAME=ptoas-${RELEASE_VER}-cp311-none-manylinux_2_34_${ARCH}.whl ARG CLI_TAR_NAME=ptoas-bin-${ARCH}.tar.gz diff --git a/examples/aot/flash_attention/140tflops/README.md b/examples/aot/flash_attention/140tflops/README.md new file mode 100644 index 00000000..98c64298 --- /dev/null +++ b/examples/aot/flash_attention/140tflops/README.md @@ -0,0 +1,82 @@ +# Flash Attention 140 TFLOP/s DSL Builders + +This directory has two PTODSL Flash Attention builders: + +- `fa_dsl_builder.py`: default `TILE_S1=256` builder. +- `fa_dsl_builder_tile512.py`: experimental `TILE_S1=512` builder. + +Both compile scripts write the same runtime artifact: + +```text +build_artifacts/fa_dsl.so +``` + +`run.py` always loads that file. Compile first, then run. + +## Build + +Default 256-tile kernel: + +```bash +bash compile.sh +``` + +Experimental 512-tile kernel: + +```bash +bash compile_tile512.sh +``` + +The 512-tile builder uses `TILE_S1=512` and `QK_PRELOAD=4`, so it requires +`S1 >= 2048`. The default `run.py` sweep skips `S1=1024` for this builder. + +## Run + +Run one or more specific sequence lengths: + +```bash +python run.py --s1-values 8192 +python run.py --s1-values 8192,131072 +``` + +Benchmark PTODSL perf only for one sequence length: + +```bash +python run.py --perf-mode 131072 +``` + +## Vector Barrier Removal Experiment + +Both compile scripts accept selected generated-C++ vector barrier removals: + +```bash +bash compile.sh --remove-vec-barriers line1,line2,... +bash compile_tile512.sh --remove-vec-barriers line1,line2,... +``` + +This only removes lines containing: + +```cpp +pipe_barrier(PIPE_V); +``` + +The patched C++ is emitted as: + +```text +build_artifacts/fa_dsl_patched.cpp +``` + +The compiled shared object is still: + +```text +build_artifacts/fa_dsl.so +``` + +### Known Useful 256-Tile Variant + +Use: + +```bash +bash compile.sh --remove-vec-barriers 1264,1267,1272,1275,1279,1282,1311,1313,1316,1320,1322,1325,1328,1330,1333,1362,1364,1367,1371,1373,1376,1379,1381,1384,1390 +python run.py --perf-mode 131072 +``` diff --git a/examples/aot/flash_attention/140tflops/caller.cpp b/examples/aot/flash_attention/140tflops/caller.cpp index d17a0ec8..0dfe6530 100644 --- a/examples/aot/flash_attention/140tflops/caller.cpp +++ b/examples/aot/flash_attention/140tflops/caller.cpp @@ -25,7 +25,7 @@ extern "C" void call_kernel( (void)fftsLen; call_both<<>>( - (__gm__ int64_t *)fftsAddr, + (__gm__ uint64_t *)fftsAddr, (__gm__ float *)gmSlotBuffer, (__gm__ half *)gmSlotBuffer, (__gm__ half *)q, diff --git a/examples/aot/flash_attention/140tflops/compile.sh b/examples/aot/flash_attention/140tflops/compile.sh index 4e5a6a6b..80f41ab0 100755 --- a/examples/aot/flash_attention/140tflops/compile.sh +++ b/examples/aot/flash_attention/140tflops/compile.sh @@ -2,36 +2,23 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/compile_common.sh" + ARTIFACT_DIR="${SCRIPT_DIR}/build_artifacts" PTO_LIB_PATH="${PTO_LIB_PATH:-/sources/pto-isa}" NPU_ARCH="${NPU_ARCH:-dav-2201}" - +PTO_LEVEL="${PTO_LEVEL:-}" MLIR_PATH="${ARTIFACT_DIR}/fa_dsl.mlir" GENERATED_CPP="${ARTIFACT_DIR}/fa_dsl.cpp" +PATCHED_CPP="${ARTIFACT_DIR}/fa_dsl_patched.cpp" LIB_PATH="${ARTIFACT_DIR}/fa_dsl.so" -FA_DSL_BUILDER="${FA_DSL_BUILDER:-fa_dsl_builder.py}" -BUILDER_PATH="${SCRIPT_DIR}/${FA_DSL_BUILDER}" -PTOAS_SYNC_ARGS=(--enable-insert-sync) +RUNTIME_BUILDER_PATH="${ARTIFACT_DIR}/fa_dsl_runtime_builder.py" +BUILDER_PATH="${SCRIPT_DIR}/fa_dsl_builder.py" -if [[ $# -gt 1 ]]; then - echo "Usage: $0 [--manual-sync]" >&2 - exit 2 -fi - -if [[ $# -eq 1 ]]; then - case "$1" in - --manual-sync) - PTOAS_SYNC_ARGS=() - ;; - *) - echo "Usage: $0 [--manual-sync]" >&2 - exit 2 - ;; - esac -fi +parse_common_compile_args "$@" mkdir -p "${ARTIFACT_DIR}" -rm -f "${MLIR_PATH}" "${GENERATED_CPP}" "${LIB_PATH}" +rm -f "${MLIR_PATH}" "${GENERATED_CPP}" "${PATCHED_CPP}" "${LIB_PATH}" "${RUNTIME_BUILDER_PATH}" if [[ ! -f "${BUILDER_PATH}" ]]; then echo "Builder not found: ${BUILDER_PATH}" >&2 @@ -39,7 +26,15 @@ if [[ ! -f "${BUILDER_PATH}" ]]; then fi python "${BUILDER_PATH}" > "${MLIR_PATH}" -ptoas --pto-arch=a3 "${PTOAS_SYNC_ARGS[@]}" "${MLIR_PATH}" > "${GENERATED_CPP}" + +PTOAS_ARGS=(--pto-arch=a3) +if [[ -n "${PTO_LEVEL}" ]]; then + PTOAS_ARGS+=("--pto-level=${PTO_LEVEL}") +fi +PTOAS_ARGS+=("${PTOAS_SYNC_ARGS[@]}") + +ptoas "${PTOAS_ARGS[@]}" "${MLIR_PATH}" > "${GENERATED_CPP}" +maybe_patch_vec_barriers "${GENERATED_CPP}" "${PATCHED_CPP}" "${REMOVE_VEC_BARRIER_LINES}" bisheng \ -I"${PTO_LIB_PATH}/include" \ @@ -54,9 +49,11 @@ bisheng \ -cce-enable-mix \ --npu-arch="${NPU_ARCH}" -DMEMORY_BASE \ -std=gnu++17 \ - -DKERNEL_CPP="\"${GENERATED_CPP}\"" \ + -DKERNEL_CPP="\"${PATCHED_CPP}\"" \ "${SCRIPT_DIR}/caller.cpp" \ -o "${LIB_PATH}" echo "Generated ${GENERATED_CPP}." echo "Built ${LIB_PATH}." +cp "${BUILDER_PATH}" "${RUNTIME_BUILDER_PATH}" +echo "Runtime builder ${RUNTIME_BUILDER_PATH}." diff --git a/examples/aot/flash_attention/140tflops/compile_common.sh b/examples/aot/flash_attention/140tflops/compile_common.sh new file mode 100644 index 00000000..5390ce50 --- /dev/null +++ b/examples/aot/flash_attention/140tflops/compile_common.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +parse_common_compile_args() { + PTOAS_SYNC_ARGS=(--enable-insert-sync) + REMOVE_VEC_BARRIER_LINES="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --remove-vec-barriers) + if [[ $# -lt 2 || -z "$2" ]]; then + echo "--remove-vec-barriers requires a comma-separated line list" >&2 + exit 2 + fi + REMOVE_VEC_BARRIER_LINES="$2" + shift 2 + ;; + *) + echo "Usage: $0 [--remove-vec-barriers line1,line2,...]" >&2 + exit 2 + ;; + esac + done +} + +maybe_patch_vec_barriers() { + local src_cpp="$1" + local dst_cpp="$2" + local raw_lines="$3" + + if [[ -z "${raw_lines}" ]]; then + PATCHED_CPP="${src_cpp}" + return + fi + + python - "${src_cpp}" "${dst_cpp}" "${raw_lines}" <<'PY' +from pathlib import Path +import sys + +src = Path(sys.argv[1]) +dst = Path(sys.argv[2]) +remove_lines = {int(part.strip()) for part in sys.argv[3].split(",") if part.strip()} + +lines = src.read_text().splitlines() +patched = [] +for i, line in enumerate(lines, start=1): + if i in remove_lines and "pipe_barrier(PIPE_V);" in line: + patched.append(" /* removed PIPE_V barrier via --remove-vec-barriers */") + else: + patched.append(line) + +dst.write_text("\n".join(patched) + "\n") +print(f"Patched generated C++ -> {dst}") +PY + + PATCHED_CPP="${dst_cpp}" +} diff --git a/examples/aot/flash_attention/140tflops/compile_tile512.sh b/examples/aot/flash_attention/140tflops/compile_tile512.sh new file mode 100755 index 00000000..3c951e59 --- /dev/null +++ b/examples/aot/flash_attention/140tflops/compile_tile512.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/compile_common.sh" + +ARTIFACT_DIR="${SCRIPT_DIR}/build_artifacts" +PTO_LIB_PATH="${PTO_LIB_PATH:-/sources/pto-isa}" +NPU_ARCH="${NPU_ARCH:-dav-2201}" +PTO_LEVEL="${PTO_LEVEL:-}" + +MLIR_PATH="${ARTIFACT_DIR}/fa_dsl.mlir" +GENERATED_CPP="${ARTIFACT_DIR}/fa_dsl.cpp" +PATCHED_CPP="${ARTIFACT_DIR}/fa_dsl_patched.cpp" +LIB_PATH="${ARTIFACT_DIR}/fa_dsl.so" +RUNTIME_BUILDER_PATH="${ARTIFACT_DIR}/fa_dsl_runtime_builder.py" +BUILDER_PATH="${SCRIPT_DIR}/fa_dsl_builder_tile512.py" + +parse_common_compile_args "$@" + +mkdir -p "${ARTIFACT_DIR}" +rm -f "${MLIR_PATH}" "${GENERATED_CPP}" "${PATCHED_CPP}" "${LIB_PATH}" "${RUNTIME_BUILDER_PATH}" + +python "${BUILDER_PATH}" > "${MLIR_PATH}" + +PTOAS_ARGS=(--pto-arch=a3) +if [[ -n "${PTO_LEVEL}" ]]; then + PTOAS_ARGS+=("--pto-level=${PTO_LEVEL}") +fi +PTOAS_ARGS+=("${PTOAS_SYNC_ARGS[@]}") + +ptoas "${PTOAS_ARGS[@]}" "${MLIR_PATH}" > "${GENERATED_CPP}" +maybe_patch_vec_barriers "${GENERATED_CPP}" "${PATCHED_CPP}" "${REMOVE_VEC_BARRIER_LINES}" + +bisheng \ + -I"${PTO_LIB_PATH}/include" \ + -fPIC -shared -D_FORTIFY_SOURCE=2 -O2 -std=c++17 \ + -Wno-macro-redefined -Wno-ignored-attributes -fstack-protector-strong \ + -xcce -Xhost-start -Xhost-end \ + -mllvm -cce-aicore-stack-size=0x8000 \ + -mllvm -cce-aicore-function-stack-size=0x8000 \ + -mllvm -cce-aicore-record-overflow=true \ + -mllvm -cce-aicore-addr-transform \ + -mllvm -cce-aicore-dcci-insert-for-scalar=false \ + -cce-enable-mix \ + --npu-arch="${NPU_ARCH}" -DMEMORY_BASE \ + -std=gnu++17 \ + -DKERNEL_CPP="\"${PATCHED_CPP}\"" \ + "${SCRIPT_DIR}/caller.cpp" \ + -o "${LIB_PATH}" + +cp "${BUILDER_PATH}" "${RUNTIME_BUILDER_PATH}" + +echo "Generated ${GENERATED_CPP}." +echo "Built ${LIB_PATH}." +echo "Runtime builder ${RUNTIME_BUILDER_PATH}." diff --git a/examples/aot/flash_attention/140tflops/fa_dsl_builder.py b/examples/aot/flash_attention/140tflops/fa_dsl_builder.py index 192b1c5b..644bf727 100644 --- a/examples/aot/flash_attention/140tflops/fa_dsl_builder.py +++ b/examples/aot/flash_attention/140tflops/fa_dsl_builder.py @@ -16,7 +16,6 @@ SPLIT_UP_DOWN = 1 SLOT_NUM = 8 -LOCAL_SLOT_NUM = 1 QK_PRELOAD = int(os.environ.get("FA_DSL_QK_PRELOAD", "3")) EXP_RING = int(os.environ.get("FA_DSL_EXP_RING", "3")) @@ -52,7 +51,6 @@ def meta_data(): q_sub_ty = pto.SubTensorType(shape=[CUBE_S0, HEAD], dtype=fp16) kt_sub_ty = pto.SubTensorType(shape=[HEAD, CUBE_S1], dtype=fp16) v_sub_ty = pto.SubTensorType(shape=[CUBE_S1, HEAD], dtype=fp16) - o_sub_half_ty = pto.SubTensorType(shape=[S0_HALF, HEAD], dtype=fp32) o_sub_vec_ty = pto.SubTensorType(shape=[VEC_ROWS, HEAD], dtype=fp32) qk_slot_part_ty = pto.SubTensorType(shape=[CUBE_S0, CUBE_S1], dtype=fp32) qk_vec_slot_part_ty = pto.SubTensorType(shape=[VEC_ROWS, TILE_S1], dtype=fp32) @@ -456,17 +454,8 @@ def update_softmax(qk0, qk1, exp_max_first, exp_max_second): ) cEXP_RING = const(EXP_RING) - - def dispatch_exp(tile_id, builder, idx=0): - if idx == EXP_RING - 1: - builder(exp_max_first_tiles[idx], exp_max_second_tiles[idx]) - return - with pto.if_context( - (tile_id % cEXP_RING) == const(idx), has_else=True - ) as branch: - builder(exp_max_first_tiles[idx], exp_max_second_tiles[idx]) - with branch.else_context(): - dispatch_exp(tile_id, builder, idx + 1) + if EXP_RING != 3: + raise ValueError("fa_dsl_builder.py fast path expects EXP_RING == 3") qk_entry = pto.declare_global(qk_vec_slot_ty) p_entry = pto.declare_global(p_vec_slot_ty) @@ -522,15 +511,29 @@ def compute_p_update(tile_id, exp_max_first, exp_max_second): def compute_p_update_dispatch(tile_id): pop_qk_slot() - dispatch_exp( - tile_id, - lambda exp_max_first, exp_max_second: update_softmax( + mod = tile_id % cEXP_RING + with pto.if_context(mod == c0, has_else=True) as branch0: + update_softmax( qk_first, qk_second, - exp_max_first, - exp_max_second, - ), - ) + exp_max_first_tiles[0], + exp_max_second_tiles[0], + ) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + update_softmax( + qk_first, + qk_second, + exp_max_first_tiles[1], + exp_max_second_tiles[1], + ) + with branch1.else_context(): + update_softmax( + qk_first, + qk_second, + exp_max_first_tiles[2], + exp_max_second_tiles[2], + ) push_p_slot() def pop_pv_slot(): @@ -559,24 +562,22 @@ def compute_gu_init(): tile.mov(recv_second, o_second) free_pv_slot() - def compute_gu_update(exp_max_first, exp_max_second): - pop_pv_slot() + def apply_gu_update(exp_max_first, exp_max_second): tile.row_expand_mul(o_first, exp_max_first, o_first) tile.add(o_first, recv_first, o_first) tile.row_expand_mul(o_second, exp_max_second, o_second) tile.add(o_second, recv_second, o_second) - free_pv_slot() def compute_gu_update_dispatch(tile_id): pop_pv_slot() - - def update_o(exp_max_first, exp_max_second): - tile.row_expand_mul(o_first, exp_max_first, o_first) - tile.add(o_first, recv_first, o_first) - tile.row_expand_mul(o_second, exp_max_second, o_second) - tile.add(o_second, recv_second, o_second) - - dispatch_exp(tile_id, update_o) + mod = tile_id % cEXP_RING + with pto.if_context(mod == c0, has_else=True) as branch0: + apply_gu_update(exp_max_first_tiles[0], exp_max_second_tiles[0]) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + apply_gu_update(exp_max_first_tiles[1], exp_max_second_tiles[1]) + with branch1.else_context(): + apply_gu_update(exp_max_first_tiles[2], exp_max_second_tiles[2]) free_pv_slot() def compute_gu(tile_id): @@ -585,14 +586,14 @@ def compute_gu(tile_id): tile.mov(recv_first, o_first) tile.mov(recv_second, o_second) with branch.else_context(): - - def update_o(exp_max_first, exp_max_second): - tile.row_expand_mul(o_first, exp_max_first, o_first) - tile.add(o_first, recv_first, o_first) - tile.row_expand_mul(o_second, exp_max_second, o_second) - tile.add(o_second, recv_second, o_second) - - dispatch_exp(tile_id, update_o) + mod = tile_id % cEXP_RING + with pto.if_context(mod == c0, has_else=True) as branch0: + apply_gu_update(exp_max_first_tiles[0], exp_max_second_tiles[0]) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + apply_gu_update(exp_max_first_tiles[1], exp_max_second_tiles[1]) + with branch1.else_context(): + apply_gu_update(exp_max_first_tiles[2], exp_max_second_tiles[2]) free_pv_slot() compute_p_init() diff --git a/examples/aot/flash_attention/140tflops/fa_dsl_builder_tile512.py b/examples/aot/flash_attention/140tflops/fa_dsl_builder_tile512.py new file mode 100644 index 00000000..d73ae38f --- /dev/null +++ b/examples/aot/flash_attention/140tflops/fa_dsl_builder_tile512.py @@ -0,0 +1,658 @@ +from ptodsl import pto, tile, to_ir_module +from ptodsl import scalar as s + +import math +import os + +const = s.const + +CUBE_S0 = 128 +S0_HALF = CUBE_S0 // 2 +HEAD = 128 +CUBE_S1 = 128 +TILE_S1 = 512 +SUBTILES = TILE_S1 // CUBE_S1 +VEC_ROWS = S0_HALF // SUBTILES + +SPLIT_UP_DOWN = 1 +SLOT_NUM = 8 +QK_PRELOAD = int(os.environ.get("FA_DSL_QK_PRELOAD", "4")) +EXP_RING = int(os.environ.get("FA_DSL_EXP_RING", "4")) + +SLOT_SIZE_QK = CUBE_S0 * TILE_S1 * 4 +SLOT_SIZE_PV = CUBE_S0 * HEAD * 4 +SLOT_SIZE_P = CUBE_S0 * TILE_S1 * 2 + +GM_BYTES_PER_BLOCK = (SLOT_SIZE_QK + SLOT_SIZE_P + SLOT_SIZE_PV) * SLOT_NUM +GM_ELEMS_PER_BLOCK = GM_BYTES_PER_BLOCK // 4 +GM_HALF_ELEMS_PER_BLOCK = GM_BYTES_PER_BLOCK // 2 +GM_QK_OFF_F32 = 0 +GM_P_OFF_F16 = (SLOT_SIZE_QK * SLOT_NUM) // 2 +GM_PV_OFF_F32 = ((SLOT_SIZE_QK + SLOT_SIZE_P) * SLOT_NUM) // 4 + + +def meta_data(): + fp16 = pto.float16 + fp32 = pto.float32 + ffts_ty = pto.ffts_type + ptr_fp16 = pto.PtrType(fp16) + ptr_fp32 = pto.PtrType(fp32) + i64 = pto.int64 + + qkv_tensor_ty = pto.TensorType(rank=2, dtype=fp16) + o_tensor_ty = pto.TensorType(rank=2, dtype=fp32) + qk_slot_ty = pto.TensorType(shape=[CUBE_S0, TILE_S1], dtype=fp32) + qk_vec_slot_ty = pto.TensorType(shape=[S0_HALF, TILE_S1], dtype=fp32) + p_slot_ty = pto.TensorType(shape=[CUBE_S0, TILE_S1], dtype=fp16) + p_vec_slot_ty = pto.TensorType(shape=[S0_HALF, TILE_S1], dtype=fp16) + pv_slot_ty = pto.TensorType(shape=[CUBE_S0, HEAD], dtype=fp32) + pv_vec_slot_ty = pto.TensorType(shape=[S0_HALF, HEAD], dtype=fp32) + + q_sub_ty = pto.SubTensorType(shape=[CUBE_S0, HEAD], dtype=fp16) + kt_sub_ty = pto.SubTensorType(shape=[HEAD, CUBE_S1], dtype=fp16) + v_sub_ty = pto.SubTensorType(shape=[CUBE_S1, HEAD], dtype=fp16) + o_sub_vec_ty = pto.SubTensorType(shape=[VEC_ROWS, HEAD], dtype=fp32) + qk_slot_part_ty = pto.SubTensorType(shape=[CUBE_S0, CUBE_S1], dtype=fp32) + qk_vec_slot_part_ty = pto.SubTensorType(shape=[VEC_ROWS, TILE_S1], dtype=fp32) + p_slot_part_ty = pto.SubTensorType(shape=[CUBE_S0, CUBE_S1], dtype=fp16) + p_vec_slot_part_ty = pto.SubTensorType(shape=[VEC_ROWS, TILE_S1], dtype=fp16) + pv_slot_part_ty = pto.SubTensorType(shape=[CUBE_S0, HEAD], dtype=fp32) + pv_vec_slot_part_ty = pto.SubTensorType(shape=[VEC_ROWS, HEAD], dtype=fp32) + + q_mat_ty = pto.TileBufType(shape=[CUBE_S0, HEAD], dtype=fp16, memory_space="MAT") + q_left_ty = pto.TileBufType(shape=[CUBE_S0, HEAD], dtype=fp16, memory_space="LEFT") + k_mat_ty = pto.TileBufType( + shape=[HEAD, CUBE_S1], + dtype=fp16, + memory_space="MAT", + config=pto.TileBufConfig(blayout="RowMajor", slayout="ColMajor"), + ) + k_right_ty = pto.TileBufType( + shape=[HEAD, CUBE_S1], dtype=fp16, memory_space="RIGHT" + ) + qk_acc_ty = pto.TileBufType( + shape=[CUBE_S0, CUBE_S1], dtype=fp32, memory_space="ACC" + ) + + p_recv_ty = pto.TileBufType( + shape=[CUBE_S0, CUBE_S1], dtype=fp16, memory_space="MAT" + ) + p_left_ty = pto.TileBufType( + shape=[CUBE_S0, CUBE_S1], dtype=fp16, memory_space="LEFT" + ) + v_mat_ty = pto.TileBufType(shape=[CUBE_S1, HEAD], dtype=fp16, memory_space="MAT") + v_right_ty = pto.TileBufType( + shape=[CUBE_S1, HEAD], dtype=fp16, memory_space="RIGHT" + ) + pv_acc_ty = pto.TileBufType(shape=[CUBE_S0, HEAD], dtype=fp32, memory_space="ACC") + + qk_vec_ty = pto.TileBufType( + shape=[VEC_ROWS, TILE_S1], dtype=fp32, memory_space="VEC" + ) + p_fp32_ty = pto.TileBufType( + shape=[VEC_ROWS, TILE_S1], dtype=fp32, memory_space="VEC" + ) + p_fp16_ty = pto.TileBufType( + shape=[VEC_ROWS, TILE_S1], dtype=fp16, memory_space="VEC" + ) + pv_vec_ty = pto.TileBufType(shape=[VEC_ROWS, HEAD], dtype=fp32, memory_space="VEC") + o_vec_ty = pto.TileBufType(shape=[VEC_ROWS, HEAD], dtype=fp32, memory_space="VEC") + tri_ty = pto.TileBufType(shape=[VEC_ROWS, TILE_S1], dtype=fp32, memory_space="VEC") + red_ty = pto.TileBufType( + shape=[VEC_ROWS, 1], + dtype=fp32, + memory_space="VEC", + config=pto.TileBufConfig(blayout="ColMajor", slayout="NoneBox"), + ) + red_row_ty = pto.TileBufType(shape=[1, VEC_ROWS], dtype=fp32, memory_space="VEC") + + return locals() + + +@to_ir_module(meta_data=meta_data, module=True) +def module(): + @pto.func(kernel="cube") + def cube_kernel( + gm_slot_buffer: "ptr_fp32", + gm_slot_buffer_h: "ptr_fp16", + gm_q: "ptr_fp16", + gm_k: "ptr_fp16", + gm_v: "ptr_fp16", + s0_i64: "i64", + s1_i64: "i64", + ) -> None: + c0 = const(0) + c1 = const(1) + cS0 = const(CUBE_S0) + cHEAD = const(HEAD) + cTILE = const(TILE_S1) + cCUBE_S1 = const(CUBE_S1) + cGM_BLOCK = const(GM_ELEMS_PER_BLOCK) + cGM_BLOCK_H = const(GM_HALF_ELEMS_PER_BLOCK) + + bid = s.index_cast(pto.get_block_idx()) + s0 = s.index_cast(s0_i64) + s1 = s.index_cast(s1_i64) + num_tiles_s1 = s1 // cTILE + q_row_off = bid * cS0 + tiles_this_block = num_tiles_s1 + + gm_blk = pto.add_ptr(gm_slot_buffer, bid * cGM_BLOCK) + gm_blk_h = pto.add_ptr(gm_slot_buffer_h, bid * cGM_BLOCK_H) + gm_qk = pto.add_ptr(gm_blk, const(GM_QK_OFF_F32)) + gm_p = pto.add_ptr(gm_blk_h, const(GM_P_OFF_F16)) + gm_pv = pto.add_ptr(gm_blk, const(GM_PV_OFF_F32)) + + qk_slot_desc = pto.as_tensor( + qk_slot_ty, + ptr=gm_qk, + shape=[cS0, cTILE], + strides=[cTILE, c1], + ) + qk_pipe = pto.initialize_l2g2l_pipe( + dir_mask=1, + slot_size=SLOT_SIZE_QK, + slot_num=SLOT_NUM, + gm_addr=qk_slot_desc, + flag_base=0, + ) + + p_slot_desc = pto.as_tensor( + p_slot_ty, + ptr=gm_p, + shape=[cS0, cTILE], + strides=[cTILE, c1], + ) + p_pipe = pto.initialize_l2g2l_pipe( + dir_mask=2, + slot_size=SLOT_SIZE_P, + slot_num=SLOT_NUM, + gm_addr=p_slot_desc, + flag_base=2, + ) + + pv_slot_desc = pto.as_tensor( + pv_slot_ty, + ptr=gm_pv, + shape=[cS0, cHEAD], + strides=[cHEAD, c1], + ) + pv_pipe = pto.initialize_l2g2l_pipe( + dir_mask=1, + slot_size=SLOT_SIZE_PV, + slot_num=SLOT_NUM, + gm_addr=pv_slot_desc, + flag_base=4, + ) + + tv_q = pto.as_tensor( + qkv_tensor_ty, ptr=gm_q, shape=[s0, cHEAD], strides=[cHEAD, c1] + ) + tv_k = pto.as_tensor( + qkv_tensor_ty, + ptr=gm_k, + shape=[cHEAD, s1], + strides=[c1, cHEAD], + layout="DN", + ) + tv_v = pto.as_tensor( + qkv_tensor_ty, ptr=gm_v, shape=[s1, cHEAD], strides=[cHEAD, c1] + ) + + q_mat = pto.alloc_tile(q_mat_ty) + q_left = pto.alloc_tile(q_left_ty) + k_mat = pto.alloc_tile(k_mat_ty) + k_right = pto.alloc_tile(k_right_ty) + qk_acc = pto.alloc_tile(qk_acc_ty) + p_recv = pto.alloc_tile(p_recv_ty) + p_left = pto.alloc_tile(p_left_ty) + v_mat = pto.alloc_tile(v_mat_ty) + v_right = pto.alloc_tile(v_right_ty) + pv_acc = pto.alloc_tile(pv_acc_ty) + + q_view = pto.slice_view( + q_sub_ty, source=tv_q, offsets=[q_row_off, c0], sizes=[cS0, cHEAD] + ) + pto.load(q_view, q_mat) + tile.mov(q_mat, q_left) + + qk_entry = pto.declare_global(qk_slot_ty) + p_entry = pto.declare_global(p_slot_ty) + pv_entry = pto.declare_global(pv_slot_ty) + + def compute_qk_sub(tile_id, sub): + tile_col_off = tile_id * cTILE + + k_col_off = tile_col_off + const(sub * CUBE_S1) + kt_view = pto.slice_view( + kt_sub_ty, + source=tv_k, + offsets=[c0, k_col_off], + sizes=[cHEAD, cCUBE_S1], + ) + pto.load(kt_view, k_mat) + tile.mov(k_mat, k_right) + tile.matmul(q_left, k_right, qk_acc) + qk_part = pto.slice_view( + qk_slot_part_ty, + source=qk_entry, + offsets=[c0, const(sub * CUBE_S1)], + sizes=[cS0, cCUBE_S1], + ) + pto.store(qk_acc, qk_part) + + def compute_qk(tile_id): + pto.talloc(qk_entry, qk_pipe, SPLIT_UP_DOWN) + for sub in range(SUBTILES): + compute_qk_sub(tile_id, sub) + pto.tpush(qk_entry, qk_pipe, SPLIT_UP_DOWN) + + def compute_pv_sub(tile_id, sub): + tile_col_off = tile_id * cTILE + + v_col_off = tile_col_off + const(sub * CUBE_S1) + v_view = pto.slice_view( + v_sub_ty, + source=tv_v, + offsets=[v_col_off, c0], + sizes=[cCUBE_S1, cHEAD], + ) + pto.load(v_view, v_mat) + p_part = pto.slice_view( + p_slot_part_ty, + source=p_entry, + offsets=[c0, const(sub * CUBE_S1)], + sizes=[cS0, cCUBE_S1], + ) + pto.load(p_part, p_recv) + tile.mov(p_recv, p_left) + tile.mov(v_mat, v_right) + if sub == 0: + tile.matmul(p_left, v_right, pv_acc) + else: + tile.matmul_acc(pv_acc, p_left, v_right, pv_acc) + + def push_pv(): + pto.tfree(p_pipe, SPLIT_UP_DOWN, entry=p_entry) + + pto.talloc(pv_entry, pv_pipe, SPLIT_UP_DOWN) + pv_part = pto.slice_view( + pv_slot_part_ty, + source=pv_entry, + offsets=[c0, c0], + sizes=[cS0, cHEAD], + ) + pto.store(pv_acc, pv_part) + pto.tpush(pv_entry, pv_pipe, SPLIT_UP_DOWN) + + def compute_pv(tile_id): + pto.tpop_into(p_entry, p_pipe, SPLIT_UP_DOWN) + for sub in range(SUBTILES): + compute_pv_sub(tile_id, sub) + push_pv() + + def compute_qk_pv_interleaved(next_tile, tile_id): + pto.tpop_into(p_entry, p_pipe, SPLIT_UP_DOWN) + for sub in range(SUBTILES): + compute_pv_sub(tile_id, sub) + if sub == 0: + pto.talloc(qk_entry, qk_pipe, SPLIT_UP_DOWN) + if sub == SUBTILES - 1: + push_pv() + compute_qk_sub(next_tile, sub) + if sub == SUBTILES - 1: + pto.tpush(qk_entry, qk_pipe, SPLIT_UP_DOWN) + + for preload in range(QK_PRELOAD): + compute_qk(const(preload)) + + cPRELOAD = const(QK_PRELOAD) + steady_end = tiles_this_block - cPRELOAD + for tile_id in pto.range(c0, steady_end, c1): + next_tile = tile_id + cPRELOAD + compute_qk_pv_interleaved(next_tile, tile_id) + + for drain in range(QK_PRELOAD): + tile_id = steady_end + const(drain) + compute_pv(tile_id) + + @pto.func(kernel="vector") + def vector_kernel( + gm_slot_buffer: "ptr_fp32", + gm_slot_buffer_h: "ptr_fp16", + gm_o: "ptr_fp32", + s0_i64: "i64", + s1_i64: "i64", + ) -> None: + c0 = const(0) + c1 = const(1) + cS0 = const(CUBE_S0) + cS0_HALF = const(S0_HALF) + cVEC_ROWS = const(VEC_ROWS) + cHEAD = const(HEAD) + cTILE = const(TILE_S1) + cCUBE_S1 = const(CUBE_S1) + cGM_BLOCK = const(GM_ELEMS_PER_BLOCK) + cGM_BLOCK_H = const(GM_HALF_ELEMS_PER_BLOCK) + + bid = s.index_cast(pto.get_block_idx()) + sbid = s.index_cast(pto.get_subblock_idx()) + s0 = s.index_cast(s0_i64) + s1 = s.index_cast(s1_i64) + num_tiles_s1 = s1 // cTILE + q_row_off = bid * cS0 + row_off_sb = sbid * cS0_HALF + q_row_off_sb = q_row_off + row_off_sb + tiles_this_block = num_tiles_s1 + + gm_blk = pto.add_ptr(gm_slot_buffer, bid * cGM_BLOCK) + gm_blk_h = pto.add_ptr(gm_slot_buffer_h, bid * cGM_BLOCK_H) + gm_qk = pto.add_ptr(gm_blk, const(GM_QK_OFF_F32)) + gm_p = pto.add_ptr(gm_blk_h, const(GM_P_OFF_F16)) + gm_pv = pto.add_ptr(gm_blk, const(GM_PV_OFF_F32)) + + qk_slot_desc = pto.as_tensor( + qk_vec_slot_ty, + ptr=gm_qk, + shape=[cS0_HALF, cTILE], + strides=[cTILE, c1], + ) + qk_pipe = pto.initialize_l2g2l_pipe( + dir_mask=1, + slot_size=SLOT_SIZE_QK, + slot_num=SLOT_NUM, + gm_addr=qk_slot_desc, + flag_base=0, + ) + + p_slot_desc = pto.as_tensor( + p_vec_slot_ty, + ptr=gm_p, + shape=[cS0_HALF, cTILE], + strides=[cTILE, c1], + ) + p_pipe = pto.initialize_l2g2l_pipe( + dir_mask=2, + slot_size=SLOT_SIZE_P, + slot_num=SLOT_NUM, + gm_addr=p_slot_desc, + flag_base=2, + ) + + pv_slot_desc = pto.as_tensor( + pv_vec_slot_ty, + ptr=gm_pv, + shape=[cS0_HALF, cHEAD], + strides=[cHEAD, c1], + ) + pv_pipe = pto.initialize_l2g2l_pipe( + dir_mask=1, + slot_size=SLOT_SIZE_PV, + slot_num=SLOT_NUM, + gm_addr=pv_slot_desc, + flag_base=4, + ) + + tv_o = pto.as_tensor( + o_tensor_ty, ptr=gm_o, shape=[s0, cHEAD], strides=[cHEAD, c1] + ) + + qk_tile = pto.alloc_tile(qk_vec_ty) + reduce_tmp = pto.alloc_tile(p_fp32_ty) + p_fp16 = pto.alloc_tile(p_fp16_ty) + recv_tile = pto.alloc_tile(pv_vec_ty) + o_tiles = [pto.alloc_tile(o_vec_ty) for _ in range(SUBTILES)] + global_max_tiles = [pto.alloc_tile(red_ty) for _ in range(SUBTILES)] + global_sum_tiles = [pto.alloc_tile(red_ty) for _ in range(SUBTILES)] + local_max = pto.alloc_tile(red_ty) + local_sum = pto.alloc_tile(red_ty) + exp_max_tiles = [ + [pto.alloc_tile(red_ty) for _ in range(EXP_RING)] for _ in range(SUBTILES) + ] + + scale = const(1.0 / math.sqrt(HEAD), s.float32) + + def init_softmax_slice(qk, global_max, global_sum): + tile.muls(qk, scale, qk) + tile.row_max(qk, reduce_tmp, global_max) + tile.row_expand_sub(qk, global_max, qk) + tile.exp(qk, qk) + tile.row_sum(qk, reduce_tmp, global_sum) + + def update_softmax_slice(qk, exp_max, global_max, global_sum): + tile.muls(qk, scale, qk) + tile.row_max(qk, reduce_tmp, local_max) + local_max_r = tile.reshape(red_row_ty, local_max) + exp_max_r = tile.reshape(red_row_ty, exp_max) + global_max_r = tile.reshape(red_row_ty, global_max) + global_sum_r = tile.reshape(red_row_ty, global_sum) + local_sum_r = tile.reshape(red_row_ty, local_sum) + + tile.max(local_max_r, global_max_r, local_max_r) + tile.sub(global_max_r, local_max_r, exp_max_r) + tile.exp(exp_max_r, exp_max_r) + tile.mov(local_max_r, global_max_r) + tile.mul(global_sum_r, exp_max_r, global_sum_r) + + tile.row_expand_sub(qk, local_max, qk) + tile.exp(qk, qk) + tile.row_sum(qk, reduce_tmp, local_sum) + tile.add(global_sum_r, local_sum_r, global_sum_r) + + def init_softmax_row(row_slice): + init_softmax_slice( + qk_tile, + global_max_tiles[row_slice], + global_sum_tiles[row_slice], + ) + + def update_softmax_row(row_slice, exp_max): + update_softmax_slice( + qk_tile, + exp_max, + global_max_tiles[row_slice], + global_sum_tiles[row_slice], + ) + + cEXP_RING = const(EXP_RING) + # if EXP_RING != 3: + # raise ValueError("fa_dsl_builder.py fast path expects EXP_RING == 3") + + qk_entry = pto.declare_global(qk_vec_slot_ty) + p_entry = pto.declare_global(p_vec_slot_ty) + pv_entry = pto.declare_global(pv_vec_slot_ty) + + def load_qk_row(row_slice): + qk_part = pto.slice_view( + qk_vec_slot_part_ty, + source=qk_entry, + offsets=[const(row_slice * VEC_ROWS), c0], + sizes=[cVEC_ROWS, cTILE], + ) + pto.load(qk_part, qk_tile) + + def store_p_row(row_slice): + tile.cvt(qk_tile, p_fp16, rmode="cast_rint") + p_part = pto.slice_view( + p_vec_slot_part_ty, + source=p_entry, + offsets=[const(row_slice * VEC_ROWS), c0], + sizes=[cVEC_ROWS, cTILE], + ) + pto.store(p_fp16, p_part) + + def compute_p_init(): + pto.tpop_into(qk_entry, qk_pipe, SPLIT_UP_DOWN) + pto.talloc(p_entry, p_pipe, SPLIT_UP_DOWN) + for row_slice in range(SUBTILES): + load_qk_row(row_slice) + init_softmax_row(row_slice) + store_p_row(row_slice) + pto.tfree(qk_pipe, SPLIT_UP_DOWN, entry=qk_entry) + pto.tpush(p_entry, p_pipe, SPLIT_UP_DOWN) + + def compute_p_update(tile_id, ring_idx): + pto.tpop_into(qk_entry, qk_pipe, SPLIT_UP_DOWN) + pto.talloc(p_entry, p_pipe, SPLIT_UP_DOWN) + for row_slice in range(SUBTILES): + load_qk_row(row_slice) + update_softmax_row(row_slice, exp_max_tiles[row_slice][ring_idx]) + store_p_row(row_slice) + pto.tfree(qk_pipe, SPLIT_UP_DOWN, entry=qk_entry) + pto.tpush(p_entry, p_pipe, SPLIT_UP_DOWN) + + def compute_p_update_dispatch(tile_id): + mod = tile_id % cEXP_RING + with pto.if_context(mod == c0, has_else=True) as branch0: + compute_p_update(tile_id, 0) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + compute_p_update(tile_id, 1) + with branch1.else_context(): + with pto.if_context(mod == const(2), has_else=True) as branch2: + compute_p_update(tile_id, 2) + with branch2.else_context(): + compute_p_update(tile_id, 3) + + def load_pv_row(row_slice): + pv_part = pto.slice_view( + pv_vec_slot_part_ty, + source=pv_entry, + offsets=[const(row_slice * VEC_ROWS), c0], + sizes=[cVEC_ROWS, cHEAD], + ) + pto.load(pv_part, recv_tile) + + def free_pv_slot(): + pto.tfree(pv_pipe, SPLIT_UP_DOWN, entry=pv_entry) + + def compute_gu_init(): + pto.tpop_into(pv_entry, pv_pipe, SPLIT_UP_DOWN) + for row_slice in range(SUBTILES): + load_pv_row(row_slice) + tile.mov(recv_tile, o_tiles[row_slice]) + free_pv_slot() + + def apply_gu_update_row(row_slice, exp_max): + tile.row_expand_mul(o_tiles[row_slice], exp_max, o_tiles[row_slice]) + tile.add(o_tiles[row_slice], recv_tile, o_tiles[row_slice]) + + def compute_gu_update_dispatch(tile_id): + pto.tpop_into(pv_entry, pv_pipe, SPLIT_UP_DOWN) + mod = tile_id % cEXP_RING + for row_slice in range(SUBTILES): + load_pv_row(row_slice) + with pto.if_context(mod == c0, has_else=True) as branch0: + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][0]) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][1]) + with branch1.else_context(): + with pto.if_context(mod == const(2), has_else=True) as branch2: + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][2]) + with branch2.else_context(): + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][3]) + free_pv_slot() + + def compute_gu(tile_id): + pto.tpop_into(pv_entry, pv_pipe, SPLIT_UP_DOWN) + with pto.if_context(tile_id == c0, has_else=True) as branch: + for row_slice in range(SUBTILES): + load_pv_row(row_slice) + tile.mov(recv_tile, o_tiles[row_slice]) + with branch.else_context(): + mod = tile_id % cEXP_RING + for row_slice in range(SUBTILES): + load_pv_row(row_slice) + with pto.if_context(mod == c0, has_else=True) as branch0: + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][0]) + with branch0.else_context(): + with pto.if_context(mod == c1, has_else=True) as branch1: + apply_gu_update_row(row_slice, exp_max_tiles[row_slice][1]) + with branch1.else_context(): + with pto.if_context( + mod == const(2), has_else=True + ) as branch2: + apply_gu_update_row( + row_slice, exp_max_tiles[row_slice][2] + ) + with branch2.else_context(): + apply_gu_update_row( + row_slice, exp_max_tiles[row_slice][3] + ) + free_pv_slot() + + compute_p_init() + for preload in range(1, QK_PRELOAD): + compute_p_update( + const(preload), + preload % EXP_RING, + ) + + cPRELOAD = const(QK_PRELOAD) + steady_end = tiles_this_block - cPRELOAD + with pto.if_context(steady_end > c0): + compute_gu_init() + compute_p_update( + cPRELOAD, + QK_PRELOAD % EXP_RING, + ) + + for tile_id in pto.range(c1, steady_end, c1): + next_tile = tile_id + cPRELOAD + compute_gu_update_dispatch(tile_id) + compute_p_update_dispatch(next_tile) + + for drain in range(QK_PRELOAD): + tile_id = steady_end + const(drain) + compute_gu(tile_id) + + for row_slice in range(SUBTILES): + tile.row_expand_div( + o_tiles[row_slice], + global_sum_tiles[row_slice], + o_tiles[row_slice], + ) + o_view = pto.slice_view( + o_sub_vec_ty, + source=tv_o, + offsets=[q_row_off_sb + const(row_slice * VEC_ROWS), c0], + sizes=[cVEC_ROWS, cHEAD], + ) + pto.store(o_tiles[row_slice], o_view) + + @pto.func + def call_both( + ffts_addr: "ffts_ty", + gm_slot_buffer: "ptr_fp32", + gm_slot_buffer_h: "ptr_fp16", + gm_q: "ptr_fp16", + gm_k: "ptr_fp16", + gm_v: "ptr_fp16", + gm_o: "ptr_fp32", + s0_i64: "i64", + s1_i64: "i64", + ) -> None: + pto.set_ffts(ffts_addr) + pto.call( + cube_kernel, + gm_slot_buffer, + gm_slot_buffer_h, + gm_q, + gm_k, + gm_v, + s0_i64, + s1_i64, + ) + pto.call( + vector_kernel, + gm_slot_buffer, + gm_slot_buffer_h, + gm_o, + s0_i64, + s1_i64, + ) + + +if __name__ == "__main__": + print(module.operation.get_asm(print_generic_op_form=True)) diff --git a/examples/aot/flash_attention/140tflops/naive_tpush_dsl_plot.png b/examples/aot/flash_attention/140tflops/naive_tpush_dsl_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..fd51c28a8eae6179278ea9eac48707de78ba98b6 GIT binary patch literal 104736 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̥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 ctypes.c_void_p: def load_dsl_flash(lib_path: Path | None = None): if lib_path is None: lib_path = THIS_DIR / "build_artifacts" / "fa_dsl.so" - print("Compiling PTODSL flash kernel...") - subprocess.run(["bash", str(THIS_DIR / "compile.sh")], cwd=THIS_DIR, check=True) + print(f"Using default lib path: {lib_path}") + builder_path = THIS_DIR / "build_artifacts" / "fa_dsl_runtime_builder.py" if not lib_path.exists(): - raise FileNotFoundError(f"compile.sh did not create {lib_path}") + raise FileNotFoundError( + f"Missing {lib_path}; run compile.sh or compile_tile512.sh first" + ) + if not builder_path.exists(): + raise FileNotFoundError( + f"Missing {builder_path}; run compile.sh or compile_tile512.sh first" + ) - import fa_dsl_builder + spec = importlib.util.spec_from_file_location("fa_builder_runtime", builder_path) + if spec is None or spec.loader is None: + raise ImportError(f"Could not import builder from {builder_path}") + fa_dsl_builder = importlib.util.module_from_spec(spec) + spec.loader.exec_module(fa_dsl_builder) lib = ctypes.CDLL(str(lib_path)) lib.call_kernel.argtypes = [ @@ -147,6 +157,7 @@ def alloc_workspace(s0: int, s1: int, head: int, device): device=device, ) ws["o"] = torch.empty((s0, head), dtype=torch.float32, device=device) + ws["block_dim"] = block_dim def flash(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): if q.shape[1] != fa_dsl_builder.HEAD: @@ -176,18 +187,77 @@ def flash(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): ) return ws["o"] - return flash, fa_dsl_builder.TILE_S1 + def prepare_raw_runner(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): + if q.shape[1] != fa_dsl_builder.HEAD: + raise ValueError(f"HEAD must be {fa_dsl_builder.HEAD}, got {q.shape[1]}") + if q.shape[0] % fa_dsl_builder.CUBE_S0 != 0: + raise ValueError( + f"S0 must be divisible by CUBE_S0={fa_dsl_builder.CUBE_S0}" + ) + if k.shape[0] % fa_dsl_builder.TILE_S1 != 0: + raise ValueError( + f"S1 must be divisible by TILE_S1={fa_dsl_builder.TILE_S1}" + ) + + alloc_workspace(q.shape[0], k.shape[0], q.shape[1], q.device) + block_dim = ws["block_dim"] + stream_ptr = torch.npu.current_stream()._as_parameter_ + gm_slot_ptr = torch_to_ctypes(ws["gm_slot"]) + q_ptr = torch_to_ctypes(q) + k_ptr = torch_to_ctypes(k) + v_ptr = torch_to_ctypes(v) + o_ptr = torch_to_ctypes(ws["o"]) + q_rows = q.shape[0] + k_rows = k.shape[0] + + def run_raw(): + lib.call_kernel( + block_dim, + stream_ptr, + gm_slot_ptr, + q_ptr, + k_ptr, + v_ptr, + o_ptr, + q_rows, + k_rows, + ) + return ws["o"] + + return run_raw + + return flash, prepare_raw_runner, fa_dsl_builder.TILE_S1, fa_dsl_builder.QK_PRELOAD + + +def parse_s1_values(raw: str) -> list[int]: + return [int(part.strip()) for part in raw.split(",") if part.strip()] -def test_flash(): +def test_flash(s1_values: list[int] | None = None, perf_mode: bool = False): s0, head = 128 * 24, 128 - s1_values = [1024, 2048, 4096, 8192, 16384, 32768, 64 * 1024, 128 * 1024] + default_s1_values = [1024, 2048, 4096, 8192, 16384, 32768, 64 * 1024, 128 * 1024] + user_provided_s1 = s1_values is not None + if s1_values is None: + s1_values = default_s1_values is_causal = False dtype = torch.float16 q2d = torch.randn((s0, head), dtype=dtype).npu() - flash, s1_tile = load_dsl_flash() + flash, prepare_raw_runner, s1_tile, qk_preload = load_dsl_flash() + min_s1 = s1_tile * qk_preload + invalid_s1 = [s1 for s1 in s1_values if s1 < min_s1] + if invalid_s1 and user_provided_s1: + raise ValueError( + f"S1 values {invalid_s1} are too small for TILE_S1={s1_tile} " + f"and QK_PRELOAD={qk_preload}; minimum S1 is {min_s1}" + ) + if invalid_s1: + print( + f"Skipping S1 values {invalid_s1}: TILE_S1={s1_tile}, " + f"QK_PRELOAD={qk_preload}, minimum S1={min_s1}" + ) + s1_values = [s1 for s1 in s1_values if s1 >= min_s1] run_flash = lambda q, k, v: flash(q, k, v) flash_ms_values = [] @@ -196,19 +266,49 @@ def test_flash(): flash_tflops_values = [] npu_tflops_values = [] ref_tflops_values = [] + cases = [ + {"s1": s1, "flops_total": attn_flops_matmul_softmax_scale(1, s0, s1, head)} + for s1 in s1_values + ] - for s1 in s1_values: - flops_total = attn_flops_matmul_softmax_scale(1, s0, s1, head) - - # ========================== - # Inputs - # ========================== + def make_kv(s1: int): + torch.manual_seed(SEED + s1) k2d = torch.randn((s1, head), dtype=dtype).npu() v2d = torch.randn((s1, head), dtype=dtype).npu() + return k2d, v2d + + if perf_mode: + if len(cases) != 1: + raise ValueError( + "--perf-mode requires exactly one S1 via --perf-mode S1 or --s1-values" + ) + case = cases[0] + s1 = case["s1"] + flops_total = case["flops_total"] + k2d, v2d = make_kv(s1) + run_flash_raw = prepare_raw_runner(q2d, k2d, v2d) + flash_ms = do_bench( + run_flash_raw, + warmup_iters=WARMUP, + benchmark_iters=NUM_ITERATIONS, + unit="ms", + ) + print("==== PTODSL perf mode ====") + print(f"S1 : {s1}") + print(f"Causal : {is_causal}") + print(f"GFLOPs total : {flops_total//10e9}") + print( + f"{'PTODSL flash kernel':<27}: {flash_ms:.3f} ms/iter " + f"({tflops(flops_total, flash_ms):.3f} TFLOP/s)" + ) + return + + print("==== Reference phase ====") + for case in cases: + s1 = case["s1"] + flops_total = case["flops_total"] + k2d, v2d = make_kv(s1) - # ========================== - # Benchmark reference ops - # ========================== ref_ms = do_bench( lambda: fa_reference(q2d, k2d, v2d, is_causal=is_causal), warmup_iters=WARMUP, @@ -221,26 +321,52 @@ def test_flash(): benchmark_iters=NUM_ITERATIONS, unit="ms", ) + o_ref = fa_reference(q2d, k2d, v2d, is_causal=is_causal).to(torch.float32) + o_npu = fused_attention(q2d, k2d, v2d, is_causal=is_causal).to(torch.float32) + + case["ref_ms"] = ref_ms + case["npu_ms"] = npu_ms + case["o_ref"] = o_ref + case["o_npu"] = o_npu + + ref_ms_values.append(ref_ms) + npu_ms_values.append(npu_ms) + ref_tflops_values.append(tflops(flops_total, ref_ms)) + npu_tflops_values.append(tflops(flops_total, npu_ms)) + + print(f"S1 : {s1}") + print(f"Causal : {is_causal}") + print(f"GFLOPs total : {flops_total//10e9}") + print( + f"npu_fused_infer_attention : {npu_ms:.3f} ms/iter " + f"({tflops(flops_total, npu_ms):.3f} TFLOP/s)" + ) + print( + f"torch reference : {ref_ms:.3f} ms/iter " + f"({tflops(flops_total, ref_ms):.3f} TFLOP/s)" + ) + print("") + del k2d, v2d + + print("==== PTODSL kernel phase ====") + for case in cases: + s1 = case["s1"] + flops_total = case["flops_total"] + k2d, v2d = make_kv(s1) + o_ref = case["o_ref"] + o_npu = case["o_npu"] + run_flash_raw = prepare_raw_runner(q2d, k2d, v2d) + flash_ms = do_bench( - lambda: run_flash(q2d, k2d, v2d), + run_flash_raw, warmup_iters=WARMUP, benchmark_iters=NUM_ITERATIONS, unit="ms", ) - flash_ms_values.append(flash_ms) - npu_ms_values.append(npu_ms) - ref_ms_values.append(ref_ms) flash_tflops_values.append(tflops(flops_total, flash_ms)) - npu_tflops_values.append(tflops(flops_total, npu_ms)) - ref_tflops_values.append(tflops(flops_total, ref_ms)) - # ========================== - # Correctness check - # ========================== o_out = run_flash(q2d, k2d, v2d) - o_ref = fa_reference(q2d, k2d, v2d, is_causal=is_causal).to(torch.float32) - o_npu = fused_attention(q2d, k2d, v2d, is_causal=is_causal).to(torch.float32) print(f"S1 : {s1}") print(f"Causal : {is_causal}") @@ -250,18 +376,19 @@ def test_flash(): f"({tflops(flops_total, flash_ms):.3f} TFLOP/s)" ) print( - f"npu_fused_infer_attention : {npu_ms:.3f} ms/iter " - f"({tflops(flops_total, npu_ms):.3f} TFLOP/s)" + f"npu_fused_infer_attention : {case['npu_ms']:.3f} ms/iter " + f"({tflops(flops_total, case['npu_ms']):.3f} TFLOP/s)" ) print( - f"torch reference : {ref_ms:.3f} ms/iter " - f"({tflops(flops_total, ref_ms):.3f} TFLOP/s)" + f"torch reference : {case['ref_ms']:.3f} ms/iter " + f"({tflops(flops_total, case['ref_ms']):.3f} TFLOP/s)" ) torch.testing.assert_close(o_out, o_ref, rtol=1e-3, atol=1e-3) print("vs torch reference: PASSED") torch.testing.assert_close(o_out, o_npu, rtol=1e-3, atol=1e-3) print("vs npu_fused_attention: PASSED") print("") + del k2d, v2d, o_out plot_path = Path(__file__).with_name("naive_tpush_dsl_plot.png") plt.figure(figsize=(8, 5)) @@ -284,4 +411,33 @@ def test_flash(): if __name__ == "__main__": - test_flash() + parser = argparse.ArgumentParser() + parser.add_argument( + "--s1-values", + default=None, + help="Comma-separated S1 values. Default: full sweep.", + ) + parser.add_argument( + "--perf-mode", + nargs="?", + const=True, + default=False, + metavar="S1", + help=( + "Benchmark only the PTODSL raw kernel. Optionally pass one S1 directly, " + "for example: --perf-mode 131072. The old form with --s1-values still works." + ), + ) + args = parser.parse_args() + perf_mode = args.perf_mode is not False + s1_values = parse_s1_values(args.s1_values) if args.s1_values else None + if args.perf_mode is not False and args.perf_mode is not True: + if s1_values is not None: + raise ValueError( + "Pass S1 either as --perf-mode S1 or --s1-values, not both" + ) + s1_values = [int(args.perf_mode)] + test_flash( + s1_values=s1_values, + perf_mode=perf_mode, + ) diff --git a/ptodsl/api/pto.py b/ptodsl/api/pto.py index f2d80e67..94075fe1 100644 --- a/ptodsl/api/pto.py +++ b/ptodsl/api/pto.py @@ -7,6 +7,7 @@ aic_initialize_pipe, aiv_initialize_pipe, as_tensor, + bitcast, call, set_ffts, sync_set, @@ -25,6 +26,8 @@ reserve_buffer, slice_view, store, + talloc_to_aic, + talloc_to_aiv, tfree_from_aic, tfree_from_aiv, tpop_from_aic, @@ -89,9 +92,13 @@ "alloc_tile", "declare_global", "declare_tile", + "declare_global", + "bitcast", "load_scalar", "load", "store", + "talloc_to_aic", + "talloc_to_aiv", "tpush_to_aiv", "tpush_to_aic", "talloc", @@ -100,6 +107,7 @@ "tfree_from_aic", "tfree_from_aiv", "tpush", + "talloc", "tpop_into", "tpop", "tfree", diff --git a/ptodsl/api/pto_general.py b/ptodsl/api/pto_general.py index b6bfe3cf..7121cb7b 100644 --- a/ptodsl/api/pto_general.py +++ b/ptodsl/api/pto_general.py @@ -179,19 +179,29 @@ def aic_initialize_pipe( dir_mask, slot_size, gm_slot_buffer=None, # only needed on a2/a3? - c2v_consumer_buf, - v2c_consumer_buf, + gm_slot_tensor=None, + c2v_consumer_buf=None, + v2c_consumer_buf=None, id=None, + local_slot_num=None, nosplit=None, ): + kwargs = {} + if c2v_consumer_buf is not None: + kwargs["c2v_consumer_buf"] = _unwrap(c2v_consumer_buf) + if v2c_consumer_buf is not None: + kwargs["v2c_consumer_buf"] = _unwrap(v2c_consumer_buf) + if gm_slot_buffer is not None: + kwargs["gm_slot_buffer"] = _unwrap(gm_slot_buffer) + if gm_slot_tensor is not None: + kwargs["gm_slot_tensor"] = _unwrap(gm_slot_tensor) return _pto.AicInitializePipeOp( dir_mask, slot_size, - c2v_consumer_buf=_unwrap(c2v_consumer_buf), - v2c_consumer_buf=_unwrap(v2c_consumer_buf), - gm_slot_buffer=_unwrap(gm_slot_buffer), id=id, + local_slot_num=local_slot_num, nosplit=nosplit, + **kwargs, ) @@ -206,22 +216,33 @@ def aiv_initialize_pipe( dir_mask, slot_size, gm_slot_buffer=None, # only needed on a2/a3 - c2v_consumer_buf, - v2c_consumer_buf, + gm_slot_tensor=None, + c2v_consumer_buf=None, + v2c_consumer_buf=None, id=None, + local_slot_num=None, nosplit=None, ): + kwargs = {} + if c2v_consumer_buf is not None: + kwargs["c2v_consumer_buf"] = _unwrap(c2v_consumer_buf) + if v2c_consumer_buf is not None: + kwargs["v2c_consumer_buf"] = _unwrap(v2c_consumer_buf) + if gm_slot_buffer is not None: + kwargs["gm_slot_buffer"] = _unwrap(gm_slot_buffer) + if gm_slot_tensor is not None: + kwargs["gm_slot_tensor"] = _unwrap(gm_slot_tensor) return _pto.AivInitializePipeOp( dir_mask, slot_size, - c2v_consumer_buf=_unwrap(c2v_consumer_buf), - v2c_consumer_buf=_unwrap(v2c_consumer_buf), - gm_slot_buffer=_unwrap(gm_slot_buffer), id=id, + local_slot_num=local_slot_num, nosplit=nosplit, + **kwargs, ) +@with_loc def initialize_l2g2l_pipe( *, dir_mask, @@ -315,6 +336,14 @@ def tpush_to_aic(tile, split, *, id=None): return _pto.TPushToAicOp(_unwrap(tile), split, id=id) +def talloc_to_aic(entry, split, *, id=None): + return _pto.TAllocToAicOp(_unwrap(entry), split, id=id).result + + +def talloc_to_aiv(entry, split, *, id=None): + return _pto.TAllocToAivOp(_unwrap(entry), split, id=id).result + + # %recv_tile = pto.tpop_from_aic {split = 0} -> !pto.tile_buf @with_loc def tpop_from_aic(tile_type, split, *, id=None): @@ -328,13 +357,24 @@ def tpop_from_aiv(tile_type, split, *, id=None): # pto.tfree_from_aic {split = 0} @with_loc -def tfree_from_aic(split, *, id=None): - return _pto.TFreeFromAicOp(split, id=id) +def tfree_from_aic(split, *, entry=None, id=None): + kwargs = {} + if entry is not None: + kwargs["entry"] = _unwrap(entry) + return _pto.TFreeFromAicOp(split, id=id, **kwargs) + + +@with_loc +def tfree_from_aiv(split, *, entry=None, id=None): + kwargs = {} + if entry is not None: + kwargs["entry"] = _unwrap(entry) + return _pto.TFreeFromAivOp(split, id=id, **kwargs) @with_loc -def tfree_from_aiv(split, *, id=None): - return _pto.TFreeFromAivOp(split, id=id) +def bitcast(result_type, src): + return _pto.BitcastOp(result_type, _unwrap(src)).result @with_loc @@ -387,6 +427,7 @@ def print(format, scalar): "alloc_tile", "declare_global", "declare_tile", + "declare_global", "reserve_buffer", "import_reserved_buffer", "aic_initialize_pipe", @@ -395,6 +436,9 @@ def print(format, scalar): "load_scalar", "load", "store", + "bitcast", + "talloc_to_aic", + "talloc_to_aiv", "tpush_to_aiv", "tpush_to_aic", "tpop_from_aic", diff --git a/ptodsl/compiler/ir.py b/ptodsl/compiler/ir.py index bab1b9c1..890221e3 100644 --- a/ptodsl/compiler/ir.py +++ b/ptodsl/compiler/ir.py @@ -1,3 +1,4 @@ +import os import inspect from mlir.dialects import func, pto as _pto @@ -195,7 +196,8 @@ def decorator(fn): else: _define(ir_module, ctx, meta_map, fn) - ir_module.operation.verify() + if os.environ.get("PTODSL_SKIP_VERIFY", "0") != "1": + ir_module.operation.verify() return ir_module return decorator