Merged
add ops-tilelang init framework #2
ChaoyangJiAdd on Sep 7
add ops-tilelang init framework #2
Merged
Merged fromChaoyangJi/ops-tilelangmasterintocann/ops-tilelangmaster
Total: 52 files changed+4869-1
| @@ -0,0 +1,16 @@ | |||
| 1 | +root = true | ||
| 2 | + | ||
| 3 | +[*] | ||
| 4 | +charset = utf-8 | ||
| 5 | +end_of_line = lf | ||
| 6 | +insert_final_newline = true | ||
| 7 | +trim_trailing_whitespace = true | ||
| 8 | + | ||
| 9 | +[*.py] | ||
| 10 | +indent_style = space | ||
| 11 | +indent_size = 4 | ||
| 12 | + | ||
| 13 | +[*.{yaml,yml}] | ||
| 14 | +indent_style = space | ||
| 15 | +indent_size = 2 | ||
| 16 | + | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | +name: lint | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + pull_request: | ||
| 5 | + push: | ||
| 6 | + branches: [main] | ||
| 7 | + | ||
| 8 | +jobs: | ||
| 9 | + lint: | ||
| 10 | + runs-on: ubuntu-latest | ||
| 11 | + steps: | ||
| 12 | + - uses: actions/checkout@v4 | ||
| 13 | + - uses: actions/setup-python@v5 | ||
| 14 | + with: | ||
| 15 | + python-version: '3.10' | ||
| 16 | + - name: Install lint tools | ||
| 17 | + run: python -m pip install ruff | ||
| 18 | + - name: Run lint checks | ||
| 19 | + run: OPS_TILELANG_CI_TASK=lint ./scripts/run_ci.sh | ||
| @@ -0,0 +1,23 @@ | |||
| 1 | +name: package | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + pull_request: | ||
| 5 | + push: | ||
| 6 | + branches: [main] | ||
| 7 | + | ||
| 8 | +jobs: | ||
| 9 | + wheel: | ||
| 10 | + runs-on: ubuntu-latest | ||
| 11 | + steps: | ||
| 12 | + - uses: actions/checkout@v4 | ||
| 13 | + - uses: actions/setup-python@v5 | ||
| 14 | + with: | ||
| 15 | + python-version: '3.10' | ||
| 16 | + - name: Install build tools | ||
| 17 | + run: python -m pip install build setuptools wheel setuptools-scm | ||
| 18 | + - name: Build wheel | ||
| 19 | + run: OPS_TILELANG_CI_TASK=wheel ./scripts/run_ci.sh | ||
| 20 | + - name: Install wheel | ||
| 21 | + run: python -m pip install --no-deps dist/ops_tilelang-*.whl | ||
| 22 | + - name: Verify installed metadata | ||
| 23 | + run: python -c "import cann_ops_tilelang; from importlib.metadata import version; assert version('ops-tilelang')" | ||
| @@ -0,0 +1,16 @@ | |||
| 1 | +__pycache__/ | ||
| 2 | +*.py[cod] | ||
| 3 | +*.so | ||
| 4 | +*.egg-info/ | ||
| 5 | +.pytest_cache/ | ||
| 6 | +.ruff_cache/ | ||
| 7 | +.coverage | ||
| 8 | +build/ | ||
| 9 | +dist/ | ||
| 10 | +.venv/ | ||
| 11 | +venv/ | ||
| 12 | +.tilelang/ | ||
| 13 | +*.log | ||
| 14 | +src/cann_ops_tilelang/_version.py | ||
| 15 | +.claude/ | ||
| 16 | +.opencode/ | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +repos: | ||
| 2 | + - repo: https://gitcode.com/gh_mirrors/ru/ruff-pre-commit | ||
| 3 | + rev: v0.14.14 | ||
| 4 | + hooks: | ||
| 5 | + - id: ruff-check | ||
| 6 | + args: ["--output-format", "github", "--fix"] | ||
| 7 | + types: [python] | ||
| 8 | + - id: ruff-format | ||
Z | |||
| 9 | + types: [python] | ||
| 10 | + | ||
| @@ -0,0 +1,14 @@ | |||
| 1 | +# Contributing | ||
| 2 | + | ||
| 3 | +ęč°¢åäø `ops-tilelang`ćęäŗ¤ē®ååčÆ·ē”®č®¤ļ¼ | ||
| 4 | + | ||
| 5 | +1. å ¬å ± API äøę“é² TileLang å éØåÆ¹č±”ļ¼ | ||
| 6 | +2. wrapper 对 devicećdtypećshapećstride åč¾åŗē¼å²åŗåę¾å¼ę ”éŖļ¼ | ||
| 7 | +3. kernel ęä»¶åŖå å« Ascend č°åŗ¦åå ¶åæ č¦ēēęåØļ¼ | ||
| 8 | +4. ęµčÆč¦ēę£ē”®ę§ć空č¾å „ćéę³č¾å „åé¢åé č¾åŗļ¼ | ||
| 9 | +5. benchmark 使ēØēسå®ćåÆå¤ē°ēåę°ļ¼ | ||
| 10 | +6. äøåØåÆ¼å „é¶ę®µåå§å NPUćē¼čÆ kernel ęē³čÆ·č®¾å¤å åļ¼ | ||
| 11 | +7. `ruff check .`ć`ruff format --check .` åēøå ³ pytest åéčæć | ||
| 12 | + | ||
| 13 | +ē®åä¼åéč¦åę¶ę„åļ¼ē®ę čÆēćCANN/PyTorch NPU/TileLang ēę¬ćč¾å „åę°ć精度误差ćä¼åååę¶å»¶åęµčÆå½ä»¤ć | ||
| 14 | + | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +MIT License | ||
| 2 | + | ||
| 3 | +Copyright (c) 2026 ops-tilelang contributors | ||
| 4 | + | ||
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | +of this software and associated documentation files (the "Software"), to deal | ||
| 7 | +in the Software without restriction, including without limitation the rights | ||
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | +copies of the Software, and to permit persons to whom the Software is | ||
| 10 | +furnished to do so, subject to the following conditions: | ||
| 11 | + | ||
| 12 | +The above copyright notice and this permission notice shall be included in all | ||
| 13 | +copies or substantial portions of the Software. | ||
| 14 | + | ||
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | +SOFTWARE. | ||
| @@ -1,3 +1,163 @@ | |||
| 1 | # ops-tilelang | 1 | # ops-tilelang |
| 2 | 2 | ||
| 3 | -ops-tilelang ęÆ CANN 社åŗé¢åęč ¾ NPU ē TileLang é«ę§č½ē®åä»ļ¼ęØåØéčæē»äøē®”ē Kernel å®ē°ćčŖåØåęµčÆēØä¾ćę åå·„ä½č“č½½åę§č½åŗēŗæļ¼äøŗå¼åč ęä¾åÆå¤ēØćåÆéŖčÆćåÆęē»ę¼čæēē®åčµäŗ§ļ¼ę建ę ååēē®åå¼åäøäŗ¤ä»ä½ē³»ļ¼å©åęč ¾å¹³å° AI åŗēØēé«ęå¼åäøęč“ę§č½č°ä¼ć | 3 | +`ops-tilelang` ęÆé¢å Ascend å¹³å°ćåŗäŗ TileLang DSL å¼åēé«ę§č½ē®åä»åŗć锹ē®ē®ę ęÆäøŗ CANN 社åŗęä¾ē»ęęø ę°ć容ęéŖčÆćåÆęē»č°ä¼å便äŗäøå±éęēē®åå®ē°ć |
| 4 | + | ||
| 5 | +> **甬件ęÆęčå“**ļ¼ę¬ä»£ē ä»ä» ęÆę Ascend A5 å仄äøę¶ęļ¼äøäæčÆåØę“ę©ē Ascend ę¶ęäøåÆē¼čÆęę£ē”®čæč”ć | ||
| 6 | + | ||
| 7 | +å½åä»åŗęä¾åŗē”å·„ēØę”ę¶ļ¼Engram ē®åęå å« `engram_fused_weight`ć`engram_gate_fwd`ć`engram_gate_bwd` å `engram_grad_w_reduce`ļ¼č¦ēååćååååē梯度å½å¹¶ćęęē®ååę„å „åēŗ§åę°ēęćē”®å®ę§éęŗē§åćNPU ę¾åē»åćA5 ę§č½åŗēŗæćę£åēéćxdist å¤å”ē»å®ä»„ååé¶ę®µęµčÆč°åŗ¦ć | ||
| 8 | + | ||
| 9 | +## 设讔åå | ||
| 10 | + | ||
| 11 | +- **ę„å£äøč°åŗ¦å离**ļ¼Python wrapper č“č“£čÆä¹ćę ”éŖåå åļ¼`*_kernel.py` č“蓣甬件č°åŗ¦ć | ||
| 12 | +- **ęäøå”åčå**ļ¼åäøē®åęēå®ē°ć导åŗååē»č°ä¼é ē½®äæęå±éØå čć | ||
| 13 | +- **ę£ē”®ę§å äŗę§č½**ļ¼ęÆäøŖē®ååæ é”»å ·å¤ PyTorch referencećč¾¹ēęµčÆå NPU ę£ē”®ę§ęµčÆć | ||
| 14 | +- **ę§č½åÆåå½**ļ¼benchmark äøę®éęµčÆå离ļ¼åŗēŗæęęµčÆęä»¶åčÆēé群åēäæåć | ||
| 15 | +- **éæå éå¼å Øå±č”äøŗ**ļ¼å åÆ¼å „äøåå§å NPUļ¼ä¹äøē¼čÆ kernelļ¼ē¼čÆåŖåēåØē®åé¦ę¬”č°ēØę¶ć | ||
| 16 | + | ||
| 17 | +## ē®å½ē»ę | ||
| 18 | + | ||
| 19 | +```text | ||
| 20 | +ops-tilelang/ | ||
| 21 | +āāā src/cann_ops_tilelang/ | ||
| 22 | +ā āāā config.py # Ascend ēÆå¢äøčæč”é ē½® | ||
| 23 | +ā āāā utils.py # éēØēŗÆ Python å·„å · | ||
| 24 | +ā āāā engram/ # Engram ååćååå梯度å½å¹¶ē®å | ||
| 25 | +āāā tests/ | ||
| 26 | +ā āāā testing/ # åę°ēęćę°å¼ęčØå benchmark å·„å · | ||
| 27 | +ā āāā pytest_*_plugin.py # benchmark/ę¾å/seed/xdist/warning ęä»¶ | ||
| 28 | +ā āāā engram/ # ē®åęµčÆćPyTorch referencećbenchmark äøę¾åē»å | ||
| 29 | +āāā docs/ # ę¶ęåę°å¢ē®åęå | ||
| 30 | +āāā scripts/ # ę¬å°/CI ę§č”å „å£ | ||
| 31 | +āāā pyproject.toml # ę建ćä¾čµåå·„å ·é ē½® | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +## ēÆå¢č¦ę± | ||
| 35 | + | ||
| 36 | +- Python 3.10+ | ||
| 37 | +- Ascend A5 å仄äøę¶ęē NPU | ||
| 38 | +- åÆēØē CANN äø Ascend 驱åØēÆå¢ | ||
| 39 | +- äø CANN ēę¬å¹é ē PyTorch NPU | ||
| 40 | +- ęÆę Ascend target ē TileLang | ||
| 41 | + | ||
| 42 | +ä»åŗåŖå£°ęéēØē `torch` å `tilelang` Python ä¾čµć`torch_npu`ćCANN Toolkit äøé©±åØå ·ęäø„ę ¼ēēę¬é å„å ³ē³»ļ¼åŗē±éØē½²ēÆå¢å®č£ ļ¼äøåØ PyPI ä¾čµäøå¼ŗč”åŗå®ć | ||
| 43 | + | ||
| 44 | +## å®č£ | ||
| 45 | + | ||
| 46 | +### å®č£ ę¬å°å¼åēę¬ | ||
| 47 | + | ||
| 48 | +```bash | ||
| 49 | +pip install -e ".[dev]" | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +### å®č£ ååøēę¬ | ||
| 53 | + | ||
| 54 | +```bash | ||
| 55 | +pip install ops-tilelang | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +åč”å åäæęäøŗ `ops-tilelang`ļ¼Python åÆ¼å „å½å空é“äøŗ `cann_ops_tilelang`ć | ||
| 59 | + | ||
| 60 | +### ę建并å®č£ Wheel å | ||
| 61 | + | ||
| 62 | +åØå·²å®č£ å¼åä¾čµēęŗē ē®å½äøę建 wheelļ¼ | ||
| 63 | + | ||
| 64 | +```bash | ||
| 65 | +./scripts/build_wheel.sh | ||
| 66 | +``` | ||
| 67 | + | ||
| 68 | +ę建产ē©ä½äŗ `dist/`ļ¼åÆä»„ē“ę„å®č£ ļ¼ | ||
| 69 | + | ||
| 70 | +```bash | ||
| 71 | +pip install dist/ops_tilelang-*.whl | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +wheel åŖå å«čæč”ę¶ē®å代ē ļ¼äøå å« `tests/` äøēęµčÆå·„å ·å PyTorch referenceļ¼`torch_npu`ćCANN Toolkitć驱åØåäøēÆå¢å¹é ē TileLang ä»éē±ē®ę Ascend ēÆå¢ęä¾ć | ||
| 75 | + | ||
| 76 | +## 使ēØē¤ŗä¾ | ||
| 77 | + | ||
| 78 | +`engram_fused_weight` å°äø¤ä»½äŗē»“ BF16 ęéåØ FP32 äøēøä¹ļ¼ | ||
| 79 | + | ||
| 80 | +```python | ||
| 81 | +from cann_ops_tilelang import engram_fused_weight | ||
| 82 | + | ||
| 83 | +weight_hidden = torch.randn((4, 4096), dtype=torch.bfloat16, device='npu') | ||
| 84 | +weight_embed = torch.randn_like(weight_hidden) | ||
| 85 | +weight_fused = engram_fused_weight(weight_hidden, weight_embed) | ||
| 86 | +assert weight_fused.dtype == torch.float32 | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +Engram gate åååÆéę©äæåååęéēē»č®”éļ¼ååäŗ§ēč¾å „梯度åęéåē梯度ļ¼éåē± `engram_grad_w_reduce` åå°å½å¹¶å°äø¤ä»½ FP32 ęéę¢Æåŗ¦ļ¼ | ||
| 90 | + | ||
| 91 | +```python | ||
| 92 | +from cann_ops_tilelang import engram_gate_bwd, engram_gate_fwd, engram_grad_w_reduce | ||
| 93 | + | ||
| 94 | +output, dot, gate, rstd_x, rstd_k = engram_gate_fwd( | ||
| 95 | + hidden_states, kv, weight_fused, eps=1e-20, clamp_value=1e-6 | ||
| 96 | +) | ||
| 97 | +grad_x, grad_kv, grad_w_partial = engram_gate_bwd( | ||
| 98 | + grad_out, hidden_states, kv, weight_fused, dot, gate, rstd_x, rstd_k, clamp_value=1e-6 | ||
| 99 | +) | ||
| 100 | +engram_grad_w_reduce( | ||
| 101 | + grad_w_partial, weight_hidden, weight_embed, grad_weight_hidden, grad_weight_embed | ||
| 102 | +) | ||
| 103 | +``` | ||
| 104 | + | ||
| 105 | +## ęµčÆ | ||
| 106 | + | ||
| 107 | +```bash | ||
| 108 | +# CPU åÆę§č”ēåÆ¼å „äøę„å£ęµčÆ | ||
| 109 | +pytest -m 'not npu and not benchmark' | ||
| 110 | + | ||
| 111 | +# Ascend ę£ē”®ę§ęµčÆ | ||
| 112 | +pytest -m 'npu and not benchmark' | ||
| 113 | + | ||
| 114 | +# ę¾å¼čæč”ę§č½åå½ļ¼-K åÆęåę°č”Øč¾¾å¼ēé | ||
| 115 | +pytest -m benchmark --run-benchmark -K 'hidden=4096' | ||
| 116 | + | ||
| 117 | +# å·ę° A5 ę§č½åŗēŗæå NPU å³°å¼ę¾åē»å | ||
| 118 | +pytest tests/engram/test_engram_fused_weight.py -m benchmark --run-benchmark --refresh-baseline | ||
| 119 | +pytest tests/engram/test_engram_fused_weight.py -m 'not benchmark' --refresh-mem | ||
| 120 | + | ||
| 121 | +# å·ę°ę“äøŖ Engram ē®åę | ||
| 122 | +pytest tests/engram -m benchmark --run-benchmark --refresh-baseline | ||
| 123 | +pytest tests/engram -m 'not benchmark' --refresh-mem | ||
| 124 | + | ||
| 125 | +# ę åå „å£ | ||
| 126 | +./scripts/run_test.sh | ||
| 127 | +``` | ||
| 128 | + | ||
| 129 | +`OPS_TILELANG_TEST_LEVEL=0|1|2` ę§å¶ęµčÆåę°č§ęØ”ļ¼`--seed` ęä¾éēØä¾åÆå¤ē°éęŗę°ćå®ę“ēęä»¶åę°äøåŗēŗæē»“ę¤ęµēØč§ [tests/README.md](tests/README.md)ć | ||
| 130 | + | ||
| 131 | +设置 `OPS_TILELANG_PRINT_KERNEL_SOURCE=1` åÆä»„åØé¦ę¬”ę建 kernel ę¶ęå°ēęęŗē ć | ||
| 132 | + | ||
| 133 | +### CI ę§č”å „å£ | ||
| 134 | + | ||
| 135 | +`scripts/run_ci.sh` éčæ `OPS_TILELANG_CI_TASK` éę©ę§č”é¶ę®µļ¼ | ||
| 136 | + | ||
| 137 | +```bash | ||
| 138 | +OPS_TILELANG_CI_TASK=lint ./scripts/run_ci.sh | ||
| 139 | +OPS_TILELANG_CI_TASK=cpu ./scripts/run_ci.sh | ||
| 140 | +OPS_TILELANG_CI_TASK=wheel ./scripts/run_ci.sh | ||
| 141 | +OPS_TILELANG_CI_TASK=npu ./scripts/run_ci.sh | ||
| 142 | +OPS_TILELANG_CI_TASK=benchmark ./scripts/run_ci.sh tests/engram | ||
| 143 | + | ||
| 144 | +# åØ Ascend A5 åä»„äø CI runner äøę§č”å ØéØé¶ę®µ | ||
| 145 | +OPS_TILELANG_CI_TASK=all ./scripts/run_ci.sh | ||
| 146 | +``` | ||
| 147 | + | ||
| 148 | +NPU ę£ē”®ę§ęµčÆé»č®¤ęÆå” 2 äøŖ workerļ¼å¤§/ęŖē„ę¾åęµčÆé»č®¤ęÆå” 1 äøŖ workerćåÆéčæ `TILELANG_TEST_WORKERS_PER_NPU` å `TILELANG_TEST_WORKERS_PER_NPU_LARGE` č°ę“ć | ||
| 149 | + | ||
| 150 | +## ę°å¢ē®å | ||
| 151 | + | ||
| 152 | +请é 读 [docs/adding_an_operator.md](docs/adding_an_operator.md)ćę°å¢ē®åč³å°éč¦ļ¼ | ||
| 153 | + | ||
| 154 | +1. ęē”®ćē±»ååēå ¬å ± APIļ¼ | ||
| 155 | +2. Ascend TileLang kernelļ¼ | ||
| 156 | +3. PyTorch reference åę£ē”®ę§ęµčÆļ¼ | ||
| 157 | +4. 空 Tensorćéę³ dtype/shape ēč¾¹ēęµčÆļ¼ | ||
| 158 | +5. ä»£č”Øę§ shape ē benchmarkļ¼ | ||
| 159 | +6. ę攣äøåęęÆęē dtypećshapećlayout åå·²ē„éå¶ć | ||
| 160 | + | ||
| 161 | +## License | ||
| 162 | + | ||
| 163 | +MIT Licenseļ¼čÆ¦č§ [LICENSE](LICENSE)ć | ||
| @@ -0,0 +1,311 @@ | |||
| 1 | +# ę°å¢ē®åå¼åęµēØ | ||
| 2 | + | ||
| 3 | +ę¬ęęčæ°äøäøŖ Ascend TileLang ē®åä»ę„å£č®¾č®”å°ę§č½åŗēŗæå „åŗēå®ę“ęµēØćåč¾åŗē®ååÆåč `engram_fused_weight`ļ¼å å«åØę tokenćåååäøé“éååē梯度å½å¹¶ēē®åęåÆåč `engram_gate_fwd`ć`engram_gate_bwd` äø `engram_grad_w_reduce`ć | ||
| 4 | + | ||
| 5 | +## 1. ęē”®ē®åå„ēŗ¦äøå½å | ||
| 6 | + | ||
| 7 | +ē¼ē åå ē”®å®ä»„äøå å®¹ļ¼ | ||
| 8 | + | ||
| 9 | +- č¾å „ćč¾åŗååÆé `out=` åę°ēčÆä¹ļ¼ | ||
| 10 | +- ęÆęē dtypećshapećlayoutćstride å对é½č¦ę±ļ¼ | ||
| 11 | +- 空 Tensorćéčæē» Tensorćå«ååęŗ¢åŗēč”äøŗļ¼ | ||
| 12 | +- PyTorch referenceļ¼ | ||
| 13 | +- å øåćå°å°ŗåÆøćč¾¹ēåé«č“č½½ shapeļ¼ | ||
| 14 | +- ę§č½ęę ęÆę¶å»¶ć带宽ćååéčæęÆē»åęę ć | ||
| 15 | + | ||
| 16 | +ē®åćęä»¶åå ¬å¼å½ę°ē»äøä½æēØ `snake_case`ćåē§°åŗå å«åæ č¦ēäøå”åļ¼éæå čæäŗéēØēåē§°ćä¾å¦ä½æēØ `engram_fused_weight`ļ¼äøč¦ä» ä½æēØ `fused_weight`ć | ||
| 17 | + | ||
| 18 | +建议ēå éØå½åēŗ¦å®ļ¼ | ||
| 19 | + | ||
| 20 | +```text | ||
| 21 | +å ¬å¼å½ę° <operator> | ||
| 22 | +ę„å£ęä»¶ <operator>.py | ||
| 23 | +Kernel å®ē°ęä»¶ <operator>_kernel.py | ||
| 24 | +kernel generator get_<operator>_kernel | ||
| 25 | +TileLang prim_func <operator>_kernel | ||
| 26 | +ęµčÆęä»¶ test_<operator>.py | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +## 2. å建ē®å½ | ||
| 30 | + | ||
| 31 | +å¦ęē®åå±äŗå·²ęäøå”åļ¼ę¾å „评åē®å½ļ¼å¦åå å建äøå”åć仄äøä»„ `foo_bar` äøŗä¾ļ¼ | ||
| 32 | + | ||
| 33 | +```text | ||
| 34 | +src/cann_ops_tilelang/foo/ | ||
| 35 | +āāā __init__.py | ||
| 36 | +āāā foo_bar.py | ||
| 37 | +āāā foo_bar_kernel.py | ||
| 38 | + | ||
| 39 | +tests/foo/ | ||
| 40 | +āāā test_foo_bar.py | ||
| 41 | +āāā benchmark/ | ||
| 42 | +ā āāā test_foo_bar.jsonl | ||
| 43 | +āāā npu_mem_profiles/ | ||
| 44 | + āāā test_foo_bar.json | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +`benchmark/` å `npu_mem_profiles/` äøēęä»¶ē±ęµčÆčæč”ēęęę“ę°ćę§č½åŗēŗæåę¾åē»ååæ é”»éčæ pytest å·ę°å½ä»¤ēęļ¼äøč¦ęå·„ę¼ę„č®°å½ć | ||
| 48 | + | ||
| 49 | +## 3. ē¼åå ¬å ±ę„å£ | ||
| 50 | + | ||
| 51 | +`foo_bar.py` åŖå¤ēēØę·åÆč§čÆä¹å kernel launchļ¼éåøøéč¦å®ęļ¼ | ||
| 52 | + | ||
| 53 | +1. ę£ę„åę°ęÆå¦äøŗ `torch.Tensor`ļ¼ | ||
| 54 | +2. č°ēØ `require_ascend()`ļ¼ē¦ę¢éé»åéå° CPU ę CUDAļ¼ | ||
| 55 | +3. ę£ę„ NPU devicećdtypećshapećlayoutćstride å contiguousļ¼ | ||
| 56 | +4. ę£ę„甬件ę kernel č¦ę±ēéæåŗ¦ćååå对é½ēŗ¦ęļ¼ | ||
| 57 | +5. ę ”éŖåÆé `out=` ē shapećdtypećdevicećčæē»ę§åå«åå ³ē³»ļ¼ | ||
| 58 | +6. äøŗ `out is None` åé č¾åŗļ¼ | ||
| 59 | +7. åØč°ēØ kernel åå¤ē空 Tensorļ¼ | ||
| 60 | +8. éę©ē¼čÆęåę°å¹¶č·å kernelļ¼ | ||
| 61 | +9. ę ¹ę® `OPS_TILELANG_PRINT_KERNEL_SOURCE` ęå°ēęęŗē ļ¼ | ||
| 62 | +10. launch kernel å¹¶čæåē»ęć | ||
| 63 | + | ||
| 64 | +ę ”éŖå¤±č“„åŗęåŗ `TypeError`ć`ValueError` ę `RuntimeError`ćå ¬å ±ę„å£äøč¦ä½æēØåÆč¢« `python -O` å é¤ē `assert`ć | ||
| 65 | + | ||
| 66 | +ē®åē»ęå¦äøļ¼ | ||
| 67 | + | ||
| 68 | +```python | ||
| 69 | +import torch | ||
| 70 | + | ||
| 71 | +from cann_ops_tilelang.config import require_ascend, should_print_kernel_source | ||
| 72 | + | ||
| 73 | +from .foo_bar_kernel import get_foo_bar_kernel | ||
| 74 | + | ||
| 75 | + | ||
| 76 | +def foo_bar(x: torch.Tensor, *, out: torch.Tensor | None = None) -> torch.Tensor: | ||
| 77 | + require_ascend() | ||
| 78 | + # å®ę devicećdtypećshapećlayoutćalignment å out ę ”éŖć | ||
| 79 | + result = torch.empty_like(x) if out is None else out | ||
| 80 | + if x.numel() == 0: | ||
| 81 | + return result | ||
| 82 | + | ||
| 83 | + kernel = get_foo_bar_kernel(...) | ||
| 84 | + if should_print_kernel_source(): | ||
| 85 | + print(kernel.get_kernel_source()) | ||
| 86 | + kernel(x, result) | ||
| 87 | + return result | ||
| 88 | +``` | ||
| 89 | + | ||
| 90 | +äøč¦åØęØ”ååÆ¼å „é¶ę®µę„询 NPU å±ę§ę触å kernel ē¼čÆļ¼čæäŗč”äøŗåŗęØčæå°ē¬¬äøę¬”ē®åč°ēØć | ||
| 91 | + | ||
| 92 | +## 4. ē¼å Ascend TileLang kernel | ||
| 93 | + | ||
| 94 | +`foo_bar_kernel.py` åŖę¾ē½® kernel generator åē§ęč°åŗ¦č¾ å©å½ę°ć设å¤äøę øåæę°ę„询ē»äøä½æēØ `cann_ops_tilelang.config`ć | ||
| 95 | + | ||
| 96 | +ē¼čÆęåę°äøčæč”ęåę°éč¦ęē”®åŗåļ¼ | ||
| 97 | + | ||
| 98 | +- dtypećblock sizećpipeline stage åčåå¼å ³éåøøęÆē¼čÆęåę°ļ¼ | ||
| 99 | +- token ę°ćęęéæåŗ¦ēé«åŗę°ē»“åŗ¦ä¼å ä½æēØ `T.dynamic`ļ¼ | ||
| 100 | +- äøč¦ęé¢ē¹ååēčæč”ęå¼å ØéØę¾å „ generator åę°ļ¼å¦åä¼ę©å¤§ JIT ē¼åć | ||
| 101 | + | ||
| 102 | +ęØčę仄äøé”ŗåŗę¼čæļ¼ | ||
| 103 | + | ||
| 104 | +1. å®ē°äæå®ć容ęäø reference 对é½ēēę¬ļ¼ | ||
| 105 | +2. 甮认尾åć空č¾å „åéę“é¤ shape č”äøŗļ¼ | ||
| 106 | +3. å¼å „ GM/UB ååå `T.SimdVF` ę `T.SimtVF`ļ¼ | ||
| 107 | +4. ä½æēØ `T.Persistent` åę øé“ä»»å”åé ļ¼ | ||
| 108 | +5. ä½æēØ `T.annotate_buffer_versions` åå¤ stage pipelineļ¼ | ||
| 109 | +6. ęęē”®ę¶ēę¶åå¼å „ Cube/Vector MixedKernelć | ||
| 110 | + | ||
| 111 | +kernel å éØåÆä½æēØęčØéŖčÆē¼čÆęäøåéļ¼ä½ēØę·č¾å „éčÆÆåæ é”»åØå ¬å ±ę„å£äøč½¬ęęø ę°å¼åøøć | ||
| 112 | + | ||
| 113 | +## 5. 导åŗå ¬å¼ API | ||
| 114 | + | ||
| 115 | +稳å®ē®åéč¦éēŗ§ę¾å¼åƼåŗļ¼ | ||
| 116 | + | ||
| 117 | +```python | ||
| 118 | +# src/cann_ops_tilelang/foo/__init__.py | ||
| 119 | +from .foo_bar import foo_bar | ||
| 120 | + | ||
| 121 | +__all__ = ['foo_bar'] | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +ē¶åę“ę°ļ¼ | ||
| 125 | + | ||
| 126 | +- `src/cann_ops_tilelang/__init__.py`ļ¼ | ||
| 127 | + | ||
| 128 | +äøč¦ä½æēØ `import *`ćå®éŖę§ę„å£åÆä»„ęę¶åŖéčæå®ę“樔åč·Æå¾č®æé®ļ¼äøåŗęåå å „ę ¹å ć | ||
| 129 | + | ||
| 130 | +## 6. ē¼åę£ē”®ę§äøå„ēŗ¦ęµčÆ | ||
| 131 | + | ||
| 132 | +ęµčÆęä»¶åŗå¤ēØ `tests/testing/` äøēä»å ęµčÆå·„å ·ļ¼ | ||
| 133 | + | ||
| 134 | +- `get_test_level()`ļ¼čÆ»å `OPS_TILELANG_TEST_LEVEL=0|1|2`ļ¼ | ||
| 135 | +- `generate_samples()` ååē±» shape generatorļ¼ēęåēŗ§åę°ļ¼ | ||
| 136 | +- `make_param_id()`ļ¼ēę稳å®ćåÆēéē pytest IDļ¼ | ||
| 137 | +- `assert_equal()` ęå ¶ä»ę°å¼ęčØļ¼ęÆč¾ TileLang č¾åŗäø referenceļ¼ | ||
| 138 | +- ę ¹ pytest ęä»¶ļ¼ę ¹ę® `--seed` äøŗęÆäøŖ node ID 设置稳å®éęŗē§åć | ||
| 139 | + | ||
| 140 | +Ascend-only ęµčÆéč¦ę·»å `pytest.mark.npu`ļ¼ | ||
| 141 | + | ||
| 142 | +```python | ||
| 143 | +import pytest | ||
| 144 | +import torch | ||
| 145 | + | ||
| 146 | +import cann_ops_tilelang | ||
| 147 | +from cann_ops_tilelang.config import get_device | ||
| 148 | +from tests.testing.bench import make_param_id | ||
| 149 | +from tests.testing.generator import get_test_level | ||
| 150 | +from tests.testing.numeric import assert_equal | ||
| 151 | + | ||
| 152 | + | ||
| 153 | +pytestmark = pytest.mark.npu | ||
| 154 | + | ||
| 155 | + | ||
| 156 | +def generate_test_params(level: int) -> list[dict]: | ||
| 157 | + # level 0 äøŗę øåæéåļ¼1 äøŗé»č®¤éåļ¼2 äøŗå®ę“/č¾¹ēéåć | ||
| 158 | + ... | ||
| 159 | + | ||
| 160 | + | ||
| 161 | +@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id) | ||
| 162 | +def test_foo_bar(params): | ||
| 163 | + x = ... | ||
| 164 | + ref = ... | ||
| 165 | + out = cann_ops_tilelang.foo_bar(x) | ||
| 166 | + assert_equal(out, ref) | ||
| 167 | +``` | ||
| 168 | + | ||
| 169 | +č³å°č¦ēļ¼ | ||
| 170 | + | ||
| 171 | +- ęęęÆęē dtypeļ¼ | ||
| 172 | +- ę øåæćå øåćęå°ćę大åč¾¹ē shapeļ¼ | ||
| 173 | +- ę“é¤äøå°¾ååŗęÆļ¼ | ||
| 174 | +- 空 Tensorļ¼ | ||
| 175 | +- éę³ devicećdtypećshapećlayoutćstride å对é½ļ¼ | ||
| 176 | +- éčæē»č¾å „ļ¼ | ||
| 177 | +- `out=` ē¼å²åŗåäøå 许ēå«åļ¼ | ||
| 178 | +- č¾åŗ shapećdtypećdevice åę°å¼ē»ęć | ||
| 179 | + | ||
| 180 | +## 7. ę·»å benchmark | ||
| 181 | + | ||
| 182 | +benchmark äøę£ē”®ę§ęµčÆę¾åØåäøęµčÆęä»¶äøļ¼ä½æēØē»äø fixtureļ¼ | ||
| 183 | + | ||
| 184 | +```python | ||
| 185 | +from tests.testing.numeric import count_bytes | ||
| 186 | + | ||
| 187 | + | ||
| 188 | +@pytest.mark.benchmark | ||
| 189 | +@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id) | ||
| 190 | +def test_foo_bar_benchmark(benchmark_timer, benchmark_record, params): | ||
| 191 | + x = ... | ||
| 192 | + out = cann_ops_tilelang.foo_bar(x) | ||
| 193 | + | ||
| 194 | + time_us = benchmark_timer(lambda: cann_ops_tilelang.foo_bar(x)) | ||
| 195 | + num_bytes = count_bytes(x, out) | ||
| 196 | + benchmark_record( | ||
| 197 | + kernel='foo_bar', | ||
| 198 | + operation='fwd', | ||
| 199 | + params=params, | ||
| 200 | + time_us=time_us, | ||
| 201 | + bandwidth_gbs=num_bytes / time_us / 1e3, | ||
| 202 | + ) | ||
| 203 | +``` | ||
| 204 | + | ||
| 205 | +Ascend benchmark åŗå®ä½æēØ `msprof` å端并å½å „ `a5` é群ć`kernel` åę®µåæ é”»äøå ¬å¼ē®ååäøč“ļ¼ę¹åę¶éč¦éę°ēęåŗēŗæć | ||
| 206 | + | ||
| 207 | +## 8. čæč”åæ«éę£ę„äøę£ē”®ę§ęµčÆ | ||
| 208 | + | ||
| 209 | +å čæč”éęę£ę„å CPU åÆę§č”ęµčÆļ¼ | ||
| 210 | + | ||
| 211 | +```bash | ||
| 212 | +./scripts/check.sh | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +åä»ę øåæåę°éå¼å§éŖčÆē®ę ē®åļ¼ | ||
| 216 | + | ||
| 217 | +```bash | ||
| 218 | +OPS_TILELANG_TEST_LEVEL=0 \ | ||
| 219 | + pytest tests/foo/test_foo_bar.py -m 'not benchmark' | ||
| 220 | +``` | ||
| 221 | + | ||
| 222 | +ę øåæéåéčæåčæč”é»č®¤åå®ę“éåļ¼ | ||
| 223 | + | ||
| 224 | +```bash | ||
| 225 | +OPS_TILELANG_TEST_LEVEL=1 pytest tests/foo/test_foo_bar.py -m 'not benchmark' | ||
| 226 | +OPS_TILELANG_TEST_LEVEL=2 pytest tests/foo/test_foo_bar.py -m 'not benchmark' | ||
| 227 | +``` | ||
| 228 | + | ||
| 229 | +ęåéŖčÆę åäø¤é¶ę®µ xdist č°åŗ¦ļ¼ | ||
| 230 | + | ||
| 231 | +```bash | ||
| 232 | +./scripts/run_test.sh tests/foo/test_foo_bar.py | ||
| 233 | +``` | ||
| 234 | + | ||
| 235 | +第äøé¶ę®µå¹¶č”ę§č”ę®éę¾åēØä¾ļ¼ē¬¬äŗé¶ę®µä»„ę“å° worker ę§č”大ę¾åęå°ę ē»åēēØä¾ć | ||
| 236 | + | ||
| 237 | +## 9. ēę NPU ę¾åē»å | ||
| 238 | + | ||
| 239 | +ę°å¢ēØä¾åØę²”ęē»åę¶ä¼č¢«äæå®ę č®°äøŗ `large_npu_mem`ćę£ē”®ę§ēسå®åå·ę°ē»åļ¼ | ||
| 240 | + | ||
| 241 | +```bash | ||
| 242 | +pytest tests/foo/test_foo_bar.py -m 'not benchmark' --refresh-mem | ||
| 243 | +``` | ||
| 244 | + | ||
| 245 | +åŖč”„å å Øä»ē¼ŗå¤±é”¹ę¶ä½æēØļ¼ | ||
| 246 | + | ||
| 247 | +```bash | ||
| 248 | +pytest --refresh-mem --refresh-mem-missing-only | ||
| 249 | +``` | ||
| 250 | + | ||
| 251 | +ę£ę„ēęē `tests/foo/npu_mem_profiles/test_foo_bar.json`ļ¼ē”®č®¤ēØä¾ę°éćnode ID åå³°å¼ę¾ååēćé»č®¤ 15000 MB 仄äøēēØä¾čæå „第äŗé¶ę®µļ¼åÆēØ `--npu-mem-threshold` äø“ę¶č°ę“éå¼ć | ||
| 252 | + | ||
| 253 | +## 10. ēęå¹¶éŖčÆ A5 ę§č½åŗēŗæ | ||
| 254 | + | ||
| 255 | +第äøę¬”ēęåŗēŗæļ¼ | ||
| 256 | + | ||
| 257 | +```bash | ||
| 258 | +pytest tests/foo/test_foo_bar.py \ | ||
| 259 | + -m benchmark \ | ||
| 260 | + --run-benchmark \ | ||
| 261 | + --refresh-baseline | ||
| 262 | +``` | ||
| 263 | + | ||
| 264 | +å·ę°ååæ é”»ē«å³åčæč”äøę¬”åå½ęÆč¾ļ¼ē”®č®¤å ØéØč®°å½å½äøļ¼ | ||
| 265 | + | ||
| 266 | +```bash | ||
| 267 | +pytest tests/foo/test_foo_bar.py \ | ||
| 268 | + -m benchmark \ | ||
| 269 | + --run-benchmark \ | ||
| 270 | + --benchmark-regression-threshold=0.10 \ | ||
| 271 | + --benchmark-min-delta-us=1.0 | ||
| 272 | +``` | ||
| 273 | + | ||
| 274 | +åÆä½æēØ `-K` ęåę°č”Øč¾¾å¼ē¼©å°čå“ļ¼ | ||
| 275 | + | ||
| 276 | +```bash | ||
| 277 | +pytest tests/foo/test_foo_bar.py -m benchmark --run-benchmark \ | ||
| 278 | + -K 'hidden=4096 or hidden=7168' | ||
| 279 | +``` | ||
| 280 | + | ||
| 281 | +ęäŗ¤å甮认ę„åäøę²”ę missing baseline åéé¢ę regressionļ¼å¹¶ę£ę„ `tests/foo/benchmark/test_foo_bar.jsonl` åŖå å«å½åå ¬å¼ē®ååå `a5` é群记å½ć | ||
| 282 | + | ||
| 283 | +## 11. ę建äøęäŗ¤åę£ę„ | ||
| 284 | + | ||
| 285 | +å½åä»åŗęäøęä¾ęØ”åē»åę„å£ćå¤ kernel äø²čćčŖå®ä¹ backwardć`main_grad` 瓯å åč®ē»/ęØēē¼ęåŗē±äøå±ę”ę¶å®ęļ¼`src/cann_ops_tilelang/` åŖäæē稳å®ēåŗå±ē®å APIć | ||
| 286 | + | ||
| 287 | +ēŗÆ PyTorch reference äøē®åęµčÆę¾åØåäøäøå”åļ¼ä¾å¦ `tests/engram/engram_gate_ref.py`ļ¼ä» ä½äøŗę£ē”®ę§å梯度 oracleļ¼äøå¾ē±čæč”ę¶ä»£ē åÆ¼å „ć | ||
| 288 | + | ||
| 289 | +åØå½åå¼å容åØäøę建ęŗē å å wheelļ¼ | ||
| 290 | + | ||
| 291 | +```bash | ||
| 292 | +python -m build --no-isolation | ||
| 293 | +``` | ||
| 294 | + | ||
| 295 | +ęäŗ¤åę£ę„ęø åļ¼ | ||
| 296 | + | ||
| 297 | +- [ ] ęä»¶ćå ¬å¼å½ę°ćgeneratorćprim_func å benchmark `kernel` å½åäøč“ļ¼ | ||
| 298 | +- [ ] ę²”ę CPU/CUDA éé»åéęåÆ¼å „ę¶č®¾å¤åå§åļ¼ | ||
| 299 | +- [ ] å ¬å ±ę„å£å®ęč¾å „ćč¾åŗåč¾¹ēę ”éŖļ¼ | ||
| 300 | +- [ ] PyTorch reference äøåēŗ§ę£ē”®ę§ęµčÆéčæļ¼ | ||
| 301 | +- [ ] `out=`ć空 Tensor åéę³č¾å „ęęē”®č”äøŗļ¼ | ||
| 302 | +- [ ] Ruff check äø format éčæļ¼ | ||
| 303 | +- [ ] åčæēØåäø¤é¶ę®µ xdist ęµčÆéčæļ¼ | ||
| 304 | +- [ ] NPU ę¾åē»åå·²ēęå¹¶ęäŗ¤ļ¼ | ||
| 305 | +- [ ] A5 benchmark åŗēŗæå·²ēęļ¼äŗę¬”åå½ę ē¼ŗå¤±ļ¼ | ||
| 306 | +- [ ] sdist å wheel ę建ęåļ¼ | ||
| 307 | +- [ ] README ęē®åę攣åęęÆęčå“åå·²ē„éå¶ć | ||
| 308 | + | ||
| 309 | +ę§č½ēøå ³ęäŗ¤čæåŗč®°å½ SoCćCANNćPyTorch NPUćTileLang ēę¬ļ¼ä»„å shapećdtypećlayoutćč®”ę¶ę¹ę³ćä¼åååę¶å»¶åę£ē”®ę§čÆÆå·®ć | ||
| 310 | + | ||
| 311 | +ę“å¤ pytest ęä»¶åę°äøē»“ę¤å½ä»¤č§ [`tests/README.md`](../tests/README.md)ć | ||
| @@ -0,0 +1,104 @@ | |||
| 1 | +# Architecture | ||
| 2 | + | ||
| 3 | +## åå± | ||
| 4 | + | ||
| 5 | +```text | ||
| 6 | +ę”ę¶ęēØę·ä»£ē | ||
| 7 | + ā | ||
| 8 | + ā¼ | ||
| 9 | +cann_ops_tilelang å ¬å ± API | ||
| 10 | + - åę°čÆä¹ | ||
| 11 | + - device/dtype/shape/stride ę ”éŖ | ||
| 12 | + - č¾åŗå ååé | ||
| 13 | + ā | ||
| 14 | + ā¼ | ||
| 15 | +Ascend kernel generator | ||
| 16 | + - TileLang JIT ē¹ååę° | ||
| 17 | + - block/core/pipeline é ē½® | ||
| 18 | + - GM/UB ę°ę®ę¬čæ | ||
| 19 | + - SimdVF/SimtVF/Cube č®”ē® | ||
| 20 | + ā | ||
| 21 | + ā¼ | ||
| 22 | +TileLang Ascend lowering / CANN toolchain | ||
| 23 | + ā | ||
| 24 | + ā¼ | ||
| 25 | +Ascend NPU | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +## 樔åč¾¹ē | ||
| 29 | + | ||
| 30 | +### å ¬å ± API | ||
| 31 | + | ||
| 32 | +å ¬å ± API åæ é”»åŖę„ååčæå PyTorch Tensorćę éęē®åé 置对豔ćAPI å±č“č“£ļ¼ | ||
| 33 | + | ||
| 34 | +- 甮认čæč”设å¤äøŗ Ascend NPUļ¼ | ||
| 35 | +- ę ”éŖ shapećdtypećstridećlayout ååÆéč¾åŗļ¼ | ||
| 36 | +- č®”ē®äøä¼ä¾čµč®¾å¤ IR ē launch åę°ļ¼ | ||
| 37 | +- č°ēØ kernel generator å¹¶ę§č” kernelļ¼ | ||
| 38 | +- 对空 Tensor ē»åŗå®ä¹čÆå„½ēč”äøŗć | ||
| 39 | + | ||
| 40 | +### Kernel generator | ||
| 41 | + | ||
| 42 | +Kernel generator ä½æēØ `@tilelang.jit`ļ¼åę°åęäø¤ē±»ļ¼ | ||
| 43 | + | ||
| 44 | +- **ē¼čÆęåę°**ļ¼dtypećblock sizećęÆå¦åÆēØęäøŖčååęÆļ¼ | ||
| 45 | +- **čæč”ęåØęåę°**ļ¼token ę°ćåŗåéæåŗ¦ēåÆéčæ `T.dynamic` 蔨达ē结度ć | ||
| 46 | + | ||
| 47 | +äøč¦ęé«åŗę°ćé¢ē¹ååēčæč”ęå¼é误å°ę¾čæē¼čÆęåę°ļ¼å¦åä¼åƼč“ē¼čÆē¼åčØčć | ||
| 48 | + | ||
| 49 | +### é ē½®å± | ||
| 50 | + | ||
| 51 | +`config.py` åŖå¤ē Ascend ēÆå¢ę¢ęµćę øåæę°åč°čÆå¼å ³ćé ē½®ę„询使ēØę°ę§č°ēØļ¼å åÆ¼å „é¶ę®µäøå¾č®æé®č®¾å¤ć | ||
| 52 | + | ||
| 53 | +### ęµčÆå± | ||
| 54 | + | ||
| 55 | +ęµčÆč¾ å©å·„å ·å PyTorch reference å ØéØä½äŗ `tests/`ļ¼äøå±äŗčæč”ę¶å ļ¼ä¹äøä¼čæå „ wheelć | ||
| 56 | + | ||
| 57 | +ęµčÆę”ę¶ē±ę ¹ `conftest.py` ē»äøč£ č½½ęä»¶ļ¼ęÆäøŖē®åęµčÆåäøŗļ¼ | ||
| 58 | + | ||
| 59 | +1. CPU åÆę§č”ēåÆ¼å „ååę°ę ”éŖļ¼ | ||
| 60 | +2. NPU ę£ē”®ę§ęµčÆļ¼ | ||
| 61 | +3. ę¾å¼åÆēØē benchmarkļ¼ | ||
| 62 | +4. NPU å³°å¼ę¾åē»åå `large_npu_mem` čŖåØåē»ļ¼ | ||
| 63 | +5. ęęµčÆęä»¶åēćęčÆēé群éę©ēę§č½åŗēŗæć | ||
| 64 | + | ||
| 65 | +xdist worker åØåÆ¼å „ TileLang åéčæ `ASCEND_RT_VISIBLE_DEVICES` ē»å® NPUćę®éęµčÆå大/ęŖē„ę¾åęµčÆåäø¤é¶ę®µę§č”ļ¼benchmark ē¬å ęÆå¼ NPUćéęŗē§åē± `--seed` äø node ID å ±åēęļ¼äæčÆå¹¶č”č°åŗ¦ååę¶ęÆäøŖēØä¾ä»åÆå¤ē°ć | ||
| 66 | + | ||
| 67 | +### PyTorch reference å± | ||
| 68 | + | ||
| 69 | +ęÆäøŖē®ååē设å¤ę å ³ PyTorch reference äøåƹåŗęµčÆę¾åØäøčµ·ļ¼ä¾å¦ `tests/engram/engram_gate_ref.py`ćreference åŖä½äøŗę°å¼čÆä¹ć梯度åęµčÆ oracleļ¼äøåäøēäŗ§ kernel dispatchļ¼ä¹äøęę `cann_ops_tilelang` ēå ¬å¼ę„å£ćEngram gate ēåååååå仄评å®ē°äøŗåŗåć | ||
| 70 | + | ||
| 71 | +## 示ä¾č°ēØé¾ | ||
| 72 | + | ||
| 73 | +`engram_fused_weight(weight_hidden, weight_embed)` ēč°ēØé¾ļ¼ | ||
| 74 | + | ||
| 75 | +```text | ||
| 76 | +cann_ops_tilelang.engram_fused_weight | ||
| 77 | + -> validate BF16/NPU/2-D/contiguous inputs | ||
| 78 | + -> choose block size and persistent scheduling from total elements | ||
| 79 | + -> get_engram_fused_weight_kernel(hidden_size, hc_mult) | ||
| 80 | + -> allocate/reuse an FP32 output | ||
| 81 | + -> launch flattened tensors | ||
| 82 | + -> compare with the FP32 PyTorch reference | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +Engram gate č®ē»é¾č·Æļ¼ | ||
| 86 | + | ||
| 87 | +```text | ||
| 88 | +engram_fused_weight(weight_hidden, weight_embed) | ||
| 89 | + -> engram_gate_fwd(..., save_for_backward=True) | ||
| 90 | + -> output + (dot, gate_score, rstd_x, rstd_k) | ||
| 91 | + -> engram_gate_bwd(...) | ||
| 92 | + -> grad_hidden_states + grad_kv + grad_w_partial | ||
| 93 | + -> engram_grad_w_reduce(...) | ||
| 94 | + -> update grad_weight_hidden and grad_weight_embed in place | ||
| 95 | +``` | ||
| 96 | + | ||
| 97 | +## ę©å±ę¹å | ||
| 98 | + | ||
| 99 | +- `src/cann_ops_tilelang/quant/`ļ¼å ¬å ± scaling-factor layout å FP8/FP4 ē±»åē³»ē»ļ¼ | ||
| 100 | +- `src/cann_ops_tilelang/moe/`ļ¼TopKćroutingćexpert mappingļ¼ | ||
| 101 | +- `src/cann_ops_tilelang/norm/`ļ¼RMSNormćLayerNorm åčåę®å·®ļ¼ | ||
| 102 | +- `src/cann_ops_tilelang/attention/`ļ¼RoPEćKV Cachećprefill/decodeļ¼ | ||
| 103 | +- `tests/<operator>/benchmark/`ļ¼åØē®åęµčÆē®å½å ęęµčÆęä»¶ååØ JSONL ę§č½åŗēŗæć | ||
| 104 | +- `tests/<operator>/npu_mem_profiles/`ļ¼åØē®åęµčÆē®å½å ęęµčÆęä»¶ååØ JSON å³°å¼ę¾åē»åć | ||
| @@ -0,0 +1,73 @@ | |||
| 1 | +[build-system] | ||
| 2 | +requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"] | ||
| 3 | +build-backend = "setuptools.build_meta" | ||
| 4 | + | ||
| 5 | +[project] | ||
| 6 | +name = "ops-tilelang" | ||
| 7 | +dynamic = ["version"] | ||
| 8 | +description = "High-performance Ascend operators built with TileLang." | ||
| 9 | +readme = "README.md" | ||
| 10 | +requires-python = ">=3.10" | ||
| 11 | +license = "MIT" | ||
| 12 | +authors = [{name = "ops-tilelang contributors"}] | ||
| 13 | +dependencies = [ | ||
| 14 | + "torch>=2.1", | ||
| 15 | + "tilelang>=0.1.9", | ||
| 16 | +] | ||
| 17 | +classifiers = [ | ||
| 18 | + "Development Status :: 2 - Pre-Alpha", | ||
| 19 | + "Intended Audience :: Developers", | ||
| 20 | + "Programming Language :: Python :: 3", | ||
| 21 | + "Programming Language :: Python :: 3.10", | ||
| 22 | + "Programming Language :: Python :: 3.11", | ||
| 23 | + "Programming Language :: Python :: 3.12", | ||
| 24 | + "Topic :: Scientific/Engineering :: Artificial Intelligence", | ||
| 25 | +] | ||
| 26 | + | ||
| 27 | +[project.optional-dependencies] | ||
| 28 | +dev = [ | ||
| 29 | + "build>=1.2", | ||
| 30 | + "setuptools>=68", | ||
| 31 | + "setuptools-scm>=8", | ||
| 32 | + "wheel", | ||
| 33 | + "pytest>=8", | ||
| 34 | + "pytest-xdist>=3", | ||
| 35 | + "ruff>=0.12", | ||
| 36 | + "pre-commit>=4", | ||
| 37 | +] | ||
| 38 | + | ||
| 39 | +[project.urls] | ||
| 40 | +Homepage = "https://gitee.com/ascend/ops-tilelang" | ||
| 41 | +Repository = "https://gitee.com/ascend/ops-tilelang" | ||
| 42 | +Issues = "https://gitee.com/ascend/ops-tilelang/issues" | ||
| 43 | + | ||
| 44 | +[tool.setuptools.packages.find] | ||
| 45 | +where = ["src"] | ||
| 46 | +include = ["cann_ops_tilelang*"] | ||
| 47 | + | ||
| 48 | +[tool.setuptools_scm] | ||
| 49 | +version_file = "src/cann_ops_tilelang/_version.py" | ||
| 50 | +fallback_version = "0.1.0.dev0" | ||
| 51 | + | ||
| 52 | +[tool.pytest.ini_options] | ||
| 53 | +testpaths = ["tests"] | ||
| 54 | +addopts = ["--strict-markers", "-ra"] | ||
| 55 | +markers = [ | ||
| 56 | + "npu: requires an Ascend NPU runtime", | ||
| 57 | + "benchmark: performance test, disabled unless --run-benchmark is passed", | ||
| 58 | + "large_npu_mem: high or unknown peak NPU memory; run with fewer workers", | ||
| 59 | +] | ||
| 60 | +filterwarnings = [ | ||
| 61 | + "ignore:.*owner does not match the current owner.*:UserWarning", | ||
| 62 | + "ignore:.*Permission mismatch.*does not match.*:UserWarning", | ||
| 63 | +] | ||
| 64 | + | ||
| 65 | +[tool.ruff] | ||
| 66 | +line-length = 120 | ||
| 67 | +target-version = "py310" | ||
| 68 | + | ||
| 69 | +[tool.ruff.lint] | ||
| 70 | +select = ["E", "F", "I", "UP", "B", "SIM"] | ||
| 71 | + | ||
| 72 | +[tool.ruff.format] | ||
| 73 | +quote-style = "single" | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +set -euo pipefail | ||
| 3 | + | ||
| 4 | +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| 5 | +cd "${REPO_ROOT}" | ||
| 6 | + | ||
| 7 | +python -m build --wheel --no-isolation | ||
| 8 | + | ||
| 9 | +echo 'Wheel artifacts:' | ||
| 10 | +find dist -maxdepth 1 -type f -name 'ops_tilelang-*.whl' -print | sort | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +set -euo pipefail | ||
| 3 | + | ||
| 4 | +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| 5 | +cd "${REPO_ROOT}" | ||
| 6 | +export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" | ||
| 7 | + | ||
| 8 | +python -m ruff check . | ||
| 9 | +python -m ruff format --check . | ||
| 10 | +python -m pytest -m 'not npu and not benchmark' | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# Portable CI entry point. Select a task with OPS_TILELANG_CI_TASK. | ||
| 3 | + | ||
| 4 | +set -euo pipefail | ||
| 5 | +set -x | ||
| 6 | + | ||
| 7 | +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| 8 | +cd "${REPO_ROOT}" | ||
| 9 | +export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" | ||
| 10 | + | ||
| 11 | +CI_TASK=${OPS_TILELANG_CI_TASK:-all} | ||
| 12 | + | ||
| 13 | +run_lint() { | ||
| 14 | + python -m ruff check . | ||
| 15 | + python -m ruff format --check . | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +run_cpu_tests() { | ||
| 19 | + python -m pytest -m 'not npu and not benchmark' | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +run_wheel() { | ||
| 23 | + "${REPO_ROOT}/scripts/build_wheel.sh" | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +run_npu_tests() { | ||
| 27 | + "${REPO_ROOT}/scripts/run_test.sh" "$@" | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +case "${CI_TASK}" in | ||
| 31 | + lint) | ||
| 32 | + run_lint | ||
| 33 | + ;; | ||
| 34 | + cpu) | ||
| 35 | + run_cpu_tests | ||
| 36 | + ;; | ||
| 37 | + wheel) | ||
| 38 | + run_wheel | ||
| 39 | + ;; | ||
| 40 | + npu) | ||
| 41 | + run_npu_tests "$@" | ||
| 42 | + ;; | ||
| 43 | + benchmark) | ||
| 44 | + export OPS_TILELANG_RUN_BENCHMARK=1 | ||
| 45 | + run_npu_tests "$@" | ||
| 46 | + ;; | ||
| 47 | + all) | ||
| 48 | + run_lint | ||
| 49 | + run_cpu_tests | ||
| 50 | + run_wheel | ||
| 51 | + run_npu_tests "$@" | ||
| 52 | + ;; | ||
| 53 | + *) | ||
| 54 | + echo "Unknown OPS_TILELANG_CI_TASK=${CI_TASK}. Expected: lint, cpu, wheel, npu, benchmark, or all." >&2 | ||
| 55 | + exit 2 | ||
| 56 | + ;; | ||
| 57 | +esac | ||
| @@ -0,0 +1,78 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# | ||
| 3 | +# å¹¶č”čæč” ops-tilelang ę£ē”®ę§ęµčÆćē¼čÆē¶é¢äø»č¦åØ CPUļ¼å ę¤ęÆå¼ NPU | ||
| 4 | +# åÆä½æēØå¤äøŖ worker å¹¶č”ē¼čÆļ¼č大ę¾åęµčÆä½æēØę“ä½å¹¶ååŗ¦ć | ||
| 5 | +# | ||
| 6 | +# åäø¤é¶ę®µļ¼ | ||
| 7 | +# 1. ę®éęµčÆļ¼é»č®¤ęÆå¼ NPU 2 äøŖ workerļ¼ęé¤ large_npu_memć | ||
| 8 | +# 2. 大/ęŖē„ę¾åęµčÆļ¼é»č®¤ęÆå¼ NPU 1 äøŖ workerć | ||
| 9 | +# | ||
| 10 | +# OPS_TILELANG_RUN_BENCHMARK=1 ę¶åŖčæč” benchmark åå½ć | ||
| 11 | +# | ||
| 12 | + | ||
| 13 | +set -euo pipefail | ||
| 14 | +set -x | ||
| 15 | + | ||
| 16 | +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| 17 | +cd "${REPO_ROOT}" | ||
| 18 | +export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" | ||
| 19 | + | ||
| 20 | +if [[ ! -e /dev/davinci_manager ]]; then | ||
| 21 | + echo 'No Ascend NPU runtime was detected (/dev/davinci_manager is absent).' >&2 | ||
| 22 | + exit 1 | ||
| 23 | +fi | ||
| 24 | + | ||
| 25 | +if command -v npu-smi >/dev/null 2>&1; then | ||
| 26 | + npu-smi info | ||
| 27 | +else | ||
| 28 | + echo 'Warning: npu-smi is unavailable; continuing with torch_npu device detection.' >&2 | ||
| 29 | +fi | ||
| 30 | + | ||
| 31 | +NUM_NPUS=$(python -c 'import torch; import torch_npu; print(torch.npu.device_count())') | ||
| 32 | +if [[ "${NUM_NPUS}" -lt 1 ]]; then | ||
| 33 | + echo 'No Ascend NPU is visible to torch_npu.' >&2 | ||
| 34 | + exit 1 | ||
| 35 | +fi | ||
| 36 | + | ||
| 37 | +pytest_args=("$@") | ||
| 38 | + | ||
| 39 | +handle_error() { | ||
| 40 | + local exit_code=$? | ||
| 41 | + if [[ "${exit_code}" -eq 5 ]]; then | ||
| 42 | + echo 'No tests found, ignoring...' | ||
| 43 | + return 0 | ||
| 44 | + fi | ||
| 45 | + echo "Command failed with exit code ${exit_code}" >&2 | ||
| 46 | + exit "${exit_code}" | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +# Benchmark ęµéę¶ęÆå¼ NPU åŖå 许äøäøŖ workerć | ||
| 50 | +if [[ "${OPS_TILELANG_RUN_BENCHMARK:-0}" == '1' ]]; then | ||
| 51 | + echo "=== Benchmark regression tests with ${NUM_NPUS} workers (1 per NPU) ===" | ||
| 52 | + python -m pytest "${pytest_args[@]}" \ | ||
| 53 | + -n "${NUM_NPUS}" \ | ||
| 54 | + --dist load \ | ||
| 55 | + -m benchmark \ | ||
| 56 | + --run-benchmark \ | ||
| 57 | + --benchmark-regression-threshold 0.10 \ | ||
| 58 | + --benchmark-min-delta-us 1.0 || handle_error | ||
| 59 | + exit 0 | ||
| 60 | +fi | ||
| 61 | + | ||
| 62 | +WORKERS_PER_NPU=${TILELANG_TEST_WORKERS_PER_NPU:-${OPS_TILELANG_TEST_WORKERS_PER_NPU:-2}} | ||
| 63 | +NUM_WORKERS=$((NUM_NPUS * WORKERS_PER_NPU)) | ||
| 64 | +echo "=== Stage 1: Regular tests with ${NUM_WORKERS} workers (${NUM_NPUS} NPUs x ${WORKERS_PER_NPU} workers/NPU) ===" | ||
| 65 | +python -m pytest "${pytest_args[@]}" \ | ||
| 66 | + -n "${NUM_WORKERS}" \ | ||
| 67 | + --dist load \ | ||
| 68 | + -m 'not large_npu_mem and not benchmark' \ | ||
| 69 | + -x || handle_error | ||
| 70 | + | ||
| 71 | +WORKERS_PER_NPU_LARGE=${TILELANG_TEST_WORKERS_PER_NPU_LARGE:-${OPS_TILELANG_TEST_LARGE_WORKERS_PER_NPU:-1}} | ||
| 72 | +NUM_WORKERS_LARGE=$((NUM_NPUS * WORKERS_PER_NPU_LARGE)) | ||
| 73 | +echo "=== Stage 2: Large NPU memory tests with ${NUM_WORKERS_LARGE} workers (${NUM_NPUS} NPUs x ${WORKERS_PER_NPU_LARGE} workers/NPU) ===" | ||
| 74 | +python -m pytest "${pytest_args[@]}" \ | ||
| 75 | + -n "${NUM_WORKERS_LARGE}" \ | ||
| 76 | + --dist load \ | ||
| 77 | + -m 'large_npu_mem and not benchmark' \ | ||
| 78 | + -x || handle_error | ||
| @@ -0,0 +1,5 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +set -euo pipefail | ||
| 3 | + | ||
| 4 | +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) | ||
| 5 | +exec "${SCRIPT_DIR}/run_test.sh" "$@" | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +"""High-performance Ascend operators implemented with TileLang.""" | ||
| 2 | + | ||
| 3 | +__all__ = [ | ||
| 4 | + 'engram_fused_weight', | ||
| 5 | + 'engram_gate_bwd', | ||
| 6 | + 'engram_gate_fwd', | ||
| 7 | + 'engram_grad_w_reduce', | ||
| 8 | +] | ||
| 9 | + | ||
| 10 | +from .engram import engram_fused_weight, engram_gate_bwd, engram_gate_fwd, engram_grad_w_reduce | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | +"""Runtime configuration for Ascend operators. | ||
| 2 | + | ||
| 3 | +This module intentionally performs no device initialization at import time. | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +from __future__ import annotations | ||
| 7 | + | ||
| 8 | +import os | ||
| 9 | +from functools import cache, lru_cache | ||
| 10 | + | ||
| 11 | +import torch | ||
| 12 | + | ||
| 13 | +_ASCEND_DEVICE_NODE = '/dev/davinci_manager' | ||
| 14 | +_NUM_VECTOR_CORES_ENV = 'OPS_TILELANG_NUM_VECTOR_CORES' | ||
| 15 | +_PRINT_KERNEL_SOURCE_ENV = 'OPS_TILELANG_PRINT_KERNEL_SOURCE' | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +@lru_cache(maxsize=1) | ||
| 19 | +def is_ascend_available() -> bool: | ||
| 20 | + """Return whether the process appears to run in an Ascend environment.""" | ||
| 21 | + | ||
| 22 | + return os.path.exists(_ASCEND_DEVICE_NODE) and hasattr(torch, 'npu') | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +def require_ascend() -> None: | ||
| 26 | + """Raise a clear error when an Ascend runtime is unavailable.""" | ||
| 27 | + | ||
| 28 | + if not is_ascend_available(): | ||
| 29 | + raise RuntimeError( | ||
| 30 | + 'ops-tilelang requires an Ascend NPU runtime. Install a CANN-compatible ' | ||
| 31 | + 'PyTorch NPU build and run on a host exposing /dev/davinci_manager.' | ||
| 32 | + ) | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def get_device() -> torch.device: | ||
| 36 | + """Return the current Ascend device without silently selecting another backend.""" | ||
| 37 | + | ||
| 38 | + require_ascend() | ||
| 39 | + return torch.device('npu', torch.npu.current_device()) | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +@cache | ||
| 43 | +def get_num_ai_cores(device_index: int | None = None) -> int: | ||
| 44 | + """Return the number of Ascend AI/cube cores on the selected device.""" | ||
| 45 | + | ||
| 46 | + require_ascend() | ||
| 47 | + index = torch.npu.current_device() if device_index is None else device_index | ||
| 48 | + properties = torch.npu.get_device_properties(index) | ||
| 49 | + cube_cores = getattr(properties, 'cube_core_num', None) | ||
| 50 | + if cube_cores is None: | ||
| 51 | + raise RuntimeError('The current torch_npu build does not expose cube_core_num.') | ||
| 52 | + return int(cube_cores) | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +@cache | ||
| 56 | +def get_num_vector_cores(device_index: int | None = None) -> int: | ||
| 57 | + """Return the number of vector cores available to a TileLang kernel. | ||
| 58 | + | ||
| 59 | + ``OPS_TILELANG_NUM_VECTOR_CORES`` can cap the value for debugging or | ||
| 60 | + reproducible performance experiments. | ||
| 61 | + """ | ||
| 62 | + | ||
| 63 | + detected = get_num_ai_cores(device_index) * 2 | ||
| 64 | + | ||
| 65 | + override = os.getenv(_NUM_VECTOR_CORES_ENV) | ||
| 66 | + if override is None: | ||
| 67 | + return detected | ||
| 68 | + requested = int(override) | ||
| 69 | + if requested <= 0 or requested > detected: | ||
| 70 | + raise ValueError(f'{_NUM_VECTOR_CORES_ENV} must be in [1, {detected}], got {requested}.') | ||
| 71 | + return requested | ||
| 72 | + | ||
| 73 | + | ||
| 74 | +def should_print_kernel_source() -> bool: | ||
| 75 | + """Return whether generated TileLang source should be printed.""" | ||
| 76 | + | ||
| 77 | + return os.getenv(_PRINT_KERNEL_SOURCE_ENV, '0').lower() in {'1', 'true', 'yes', 'on'} | ||
| 78 | + | ||
| 79 | + | ||
| 80 | +def clear_runtime_caches() -> None: | ||
| 81 | + """Clear lazily cached hardware queries, mainly for tests.""" | ||
| 82 | + | ||
| 83 | + is_ascend_available.cache_clear() | ||
| 84 | + get_num_ai_cores.cache_clear() | ||
| 85 | + get_num_vector_cores.cache_clear() | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +"""Engram operators.""" | ||
| 2 | + | ||
| 3 | +__all__ = [ | ||
| 4 | + 'engram_fused_weight', | ||
| 5 | + 'engram_gate_bwd', | ||
| 6 | + 'engram_gate_fwd', | ||
| 7 | + 'engram_grad_w_reduce', | ||
| 8 | +] | ||
| 9 | + | ||
| 10 | +from .engram_fused_weight import engram_fused_weight | ||
| 11 | +from .engram_gate import engram_gate_bwd, engram_gate_fwd | ||
| 12 | +from .engram_grad_w_reduce import engram_grad_w_reduce | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | +"""Public interface for the Engram fused-weight operator.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import torch | ||
| 6 | + | ||
| 7 | +from cann_ops_tilelang.config import require_ascend, should_print_kernel_source | ||
| 8 | + | ||
| 9 | +from .engram_fused_weight_kernel import get_engram_fused_weight_kernel | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +def _validate_inputs( | ||
| 13 | + weight_hidden: torch.Tensor, | ||
| 14 | + weight_embed: torch.Tensor, | ||
| 15 | + out: torch.Tensor | None, | ||
| 16 | +) -> None: | ||
| 17 | + if not isinstance(weight_hidden, torch.Tensor) or not isinstance(weight_embed, torch.Tensor): | ||
| 18 | + raise TypeError('weight_hidden and weight_embed must be torch.Tensor instances.') | ||
| 19 | + require_ascend() | ||
| 20 | + if weight_hidden.device.type != 'npu' or weight_embed.device.type != 'npu': | ||
| 21 | + raise ValueError( | ||
| 22 | + f'weight_hidden and weight_embed must be NPU tensors, got {weight_hidden.device} and {weight_embed.device}.' | ||
| 23 | + ) | ||
| 24 | + if weight_hidden.device != weight_embed.device: | ||
| 25 | + raise ValueError('weight_hidden and weight_embed must be on the same NPU device.') | ||
| 26 | + if weight_hidden.ndim != 2 or weight_embed.ndim != 2: | ||
| 27 | + raise ValueError('weight_hidden and weight_embed must be two-dimensional.') | ||
| 28 | + if weight_hidden.shape != weight_embed.shape: | ||
| 29 | + raise ValueError( | ||
| 30 | + f'weight_hidden and weight_embed must have the same shape, got ' | ||
| 31 | + f'{tuple(weight_hidden.shape)} and {tuple(weight_embed.shape)}.' | ||
| 32 | + ) | ||
| 33 | + if weight_hidden.dtype != torch.bfloat16 or weight_embed.dtype != torch.bfloat16: | ||
| 34 | + raise TypeError('weight_hidden and weight_embed must both use torch.bfloat16.') | ||
| 35 | + if not weight_hidden.is_contiguous() or not weight_embed.is_contiguous(): | ||
| 36 | + raise ValueError('weight_hidden and weight_embed must be contiguous.') | ||
| 37 | + if weight_hidden.numel() and weight_hidden.numel() % 256: | ||
| 38 | + raise ValueError('the total number of input elements must be divisible by 256.') | ||
| 39 | + | ||
| 40 | + if out is None: | ||
| 41 | + return | ||
| 42 | + if not isinstance(out, torch.Tensor): | ||
| 43 | + raise TypeError('out must be a torch.Tensor when provided.') | ||
| 44 | + if out.shape != weight_hidden.shape: | ||
| 45 | + raise ValueError('out must have the same shape as weight_hidden.') | ||
| 46 | + if out.dtype != torch.float32 or out.device != weight_hidden.device: | ||
| 47 | + raise ValueError('out must be float32 and on the same NPU device as weight_hidden.') | ||
| 48 | + if not out.is_contiguous(): | ||
| 49 | + raise ValueError('out must be contiguous.') | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +def engram_fused_weight( | ||
| 53 | + weight_hidden: torch.Tensor, | ||
| 54 | + weight_embed: torch.Tensor, | ||
| 55 | + *, | ||
| 56 | + out: torch.Tensor | None = None, | ||
| 57 | +) -> torch.Tensor: | ||
| 58 | + """Multiply two BF16 Engram weights and return an FP32 tensor. | ||
| 59 | + | ||
| 60 | + The inputs must be contiguous NPU tensors with shape | ||
| 61 | + ``(hc_mult, hidden_size)``. The Ascend kernel converts both operands to | ||
| 62 | + FP32 before multiplication. | ||
| 63 | + """ | ||
| 64 | + | ||
| 65 | + _validate_inputs(weight_hidden, weight_embed, out) | ||
| 66 | + result = torch.empty_like(weight_hidden, dtype=torch.float32) if out is None else out | ||
| 67 | + if weight_hidden.numel() == 0: | ||
| 68 | + return result | ||
| 69 | + | ||
| 70 | + hc_mult, hidden_size = weight_hidden.shape | ||
| 71 | + kernel = get_engram_fused_weight_kernel(hidden_size, hc_mult) | ||
| 72 | + if should_print_kernel_source(): | ||
| 73 | + print(kernel.get_kernel_source()) | ||
| 74 | + kernel(weight_hidden.view(-1), weight_embed.view(-1), result.view(-1)) | ||
| 75 | + return result | ||
| @@ -0,0 +1,95 @@ | |||
| 1 | +"""TileLang Ascend implementation of Engram fused weight.""" | ||
| 2 | + | ||
| 3 | +import tilelang | ||
| 4 | +from tilelang import language as T | ||
| 5 | +from tilelang.language import simd as S | ||
| 6 | + | ||
| 7 | +from cann_ops_tilelang.config import get_num_vector_cores | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +@tilelang.jit | ||
| 11 | +def get_engram_fused_weight_kernel(hidden_size: int, hc_mult: int): | ||
| 12 | + """Build an elementwise BF16 x BF16 -> FP32 Ascend kernel.""" | ||
| 13 | + | ||
| 14 | + total_elements = hc_mult * hidden_size | ||
| 15 | + if total_elements <= 12288: | ||
| 16 | + block_size = 512 | ||
| 17 | + elif total_elements <= 131072: | ||
| 18 | + block_size = 2048 | ||
| 19 | + elif total_elements <= 524288: | ||
| 20 | + block_size = 4096 | ||
| 21 | + else: | ||
| 22 | + block_size = 8192 | ||
| 23 | + while total_elements % block_size != 0: | ||
| 24 | + block_size //= 2 | ||
| 25 | + assert block_size >= 256 | ||
| 26 | + | ||
| 27 | + vector_size = 64 | ||
| 28 | + num_vectors = block_size // vector_size | ||
| 29 | + num_blocks = total_elements // block_size | ||
| 30 | + num_vec_cores = get_num_vector_cores() | ||
| 31 | + use_persistent = num_blocks > num_vec_cores | ||
| 32 | + num_cores = min(num_vec_cores, num_blocks) | ||
| 33 | + | ||
| 34 | + @T.prim_func | ||
| 35 | + def engram_fused_weight_kernel( | ||
| 36 | + weight_hidden: T.Tensor[(total_elements,), T.bfloat16], | ||
| 37 | + weight_embed: T.Tensor[(total_elements,), T.bfloat16], | ||
| 38 | + weight_fused: T.Tensor[(total_elements,), T.float32], | ||
| 39 | + ) -> None: | ||
| 40 | + if use_persistent: | ||
| 41 | + with T.Kernel(num_cores) as core_id: | ||
| 42 | + hidden_ub = T.alloc_shared((block_size,), T.bfloat16) | ||
| 43 | + embed_ub = T.alloc_shared((block_size,), T.bfloat16) | ||
| 44 | + fused_ub = T.alloc_shared((block_size,), T.float32) | ||
| 45 | + T.annotate_buffer_versions({hidden_ub: 2, embed_ub: 2, fused_ub: 2}) | ||
| 46 | + for block in T.Persistent( | ||
| 47 | + [num_blocks], | ||
| 48 | + num_cores, | ||
| 49 | + core_id, | ||
| 50 | + group_size=1, | ||
| 51 | + num_stages=2, | ||
| 52 | + ): | ||
| 53 | + offset = block * block_size | ||
| 54 | + T.copy(weight_hidden[offset : offset + block_size], hidden_ub[:]) | ||
| 55 | + T.copy(weight_embed[offset : offset + block_size], embed_ub[:]) | ||
| 56 | + with T.SimdVF(): | ||
| 57 | + for vector in range(num_vectors): | ||
| 58 | + vector_offset = vector * vector_size | ||
| 59 | + hidden_vector = S.vcvt( | ||
| 60 | + S.vld(hidden_ub[vector_offset], dist='UNPK_B16'), | ||
| 61 | + T.float32, | ||
| 62 | + part=0, | ||
| 63 | + ) | ||
| 64 | + embed_vector = S.vcvt( | ||
| 65 | + S.vld(embed_ub[vector_offset], dist='UNPK_B16'), | ||
| 66 | + T.float32, | ||
| 67 | + part=0, | ||
| 68 | + ) | ||
| 69 | + S.vsts(fused_ub[vector_offset], S.vmul(hidden_vector, embed_vector)) | ||
| 70 | + T.copy(fused_ub[:], weight_fused[offset : offset + block_size]) | ||
| 71 | + else: | ||
| 72 | + with T.Kernel(num_blocks) as block: | ||
| 73 | + hidden_ub = T.alloc_shared((block_size,), T.bfloat16) | ||
| 74 | + embed_ub = T.alloc_shared((block_size,), T.bfloat16) | ||
| 75 | + fused_ub = T.alloc_shared((block_size,), T.float32) | ||
| 76 | + offset = block * block_size | ||
| 77 | + T.copy(weight_hidden[offset : offset + block_size], hidden_ub[:]) | ||
| 78 | + T.copy(weight_embed[offset : offset + block_size], embed_ub[:]) | ||
| 79 | + with T.SimdVF(): | ||
| 80 | + for vector in range(num_vectors): | ||
| 81 | + vector_offset = vector * vector_size | ||
| 82 | + hidden_vector = S.vcvt( | ||
| 83 | + S.vld(hidden_ub[vector_offset], dist='UNPK_B16'), | ||
| 84 | + T.float32, | ||
| 85 | + part=0, | ||
| 86 | + ) | ||
| 87 | + embed_vector = S.vcvt( | ||
| 88 | + S.vld(embed_ub[vector_offset], dist='UNPK_B16'), | ||
| 89 | + T.float32, | ||
| 90 | + part=0, | ||
| 91 | + ) | ||
| 92 | + S.vsts(fused_ub[vector_offset], S.vmul(hidden_vector, embed_vector)) | ||
| 93 | + T.copy(fused_ub[:], weight_fused[offset : offset + block_size]) | ||
| 94 | + | ||
| 95 | + return engram_fused_weight_kernel | ||
| @@ -0,0 +1,236 @@ | |||
| 1 | +"""Public interfaces for the Engram gate forward and backward operators.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +from numbers import Real | ||
| 6 | + | ||
| 7 | +import torch | ||
| 8 | + | ||
| 9 | +from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source | ||
| 10 | + | ||
| 11 | +from .engram_gate_kernel import get_engram_gate_bwd_kernel, get_engram_gate_fwd_kernel | ||
| 12 | + | ||
| 13 | +_SUPPORTED_HC_MULT = 4 | ||
| 14 | +_NUM_GRAD_V_CORES = 12 | ||
| 15 | + | ||
| 16 | + | ||
| 17 | +def _check_real(name: str, value: float, *, positive: bool) -> None: | ||
| 18 | + if not isinstance(value, Real): | ||
| 19 | + raise TypeError(f'{name} must be a real number.') | ||
| 20 | + if positive and value <= 0: | ||
| 21 | + raise ValueError(f'{name} must be greater than zero.') | ||
| 22 | + if not positive and value < 0: | ||
| 23 | + raise ValueError(f'{name} must be non-negative.') | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +def _check_npu_tensor( | ||
| 27 | + name: str, | ||
| 28 | + tensor: torch.Tensor, | ||
| 29 | + *, | ||
| 30 | + dtype: torch.dtype, | ||
| 31 | + device: torch.device | None = None, | ||
| 32 | +) -> None: | ||
| 33 | + if not isinstance(tensor, torch.Tensor): | ||
| 34 | + raise TypeError(f'{name} must be a torch.Tensor.') | ||
| 35 | + if tensor.device.type != 'npu': | ||
| 36 | + raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.') | ||
| 37 | + if device is not None and tensor.device != device: | ||
| 38 | + raise ValueError(f'{name} must be on {device}, got {tensor.device}.') | ||
| 39 | + if tensor.dtype != dtype: | ||
| 40 | + raise TypeError(f'{name} must use {dtype}, got {tensor.dtype}.') | ||
| 41 | + if not tensor.is_contiguous(): | ||
| 42 | + raise ValueError(f'{name} must be contiguous.') | ||
| 43 | + | ||
| 44 | + | ||
| 45 | +def _validate_common_inputs( | ||
| 46 | + hidden_states: torch.Tensor, | ||
| 47 | + kv: torch.Tensor, | ||
| 48 | + weight_fused: torch.Tensor, | ||
| 49 | +) -> tuple[int, int, int]: | ||
| 50 | + require_ascend() | ||
| 51 | + _check_npu_tensor('hidden_states', hidden_states, dtype=torch.bfloat16) | ||
| 52 | + _check_npu_tensor('kv', kv, dtype=torch.bfloat16, device=hidden_states.device) | ||
| 53 | + _check_npu_tensor('weight_fused', weight_fused, dtype=torch.float32, device=hidden_states.device) | ||
| 54 | + | ||
| 55 | + if hidden_states.ndim != 3: | ||
| 56 | + raise ValueError('hidden_states must have shape (num_tokens, hc_mult, hidden_size).') | ||
| 57 | + num_tokens, hc_mult, hidden_size = hidden_states.shape | ||
| 58 | + if hc_mult != _SUPPORTED_HC_MULT: | ||
| 59 | + raise ValueError(f'Engram gate currently requires hc_mult == {_SUPPORTED_HC_MULT}, got {hc_mult}.') | ||
| 60 | + if hidden_size == 0 or hidden_size % 64: | ||
| 61 | + raise ValueError(f'hidden_size must be a positive multiple of 64, got {hidden_size}.') | ||
| 62 | + if kv.shape != (num_tokens, (hc_mult + 1) * hidden_size): | ||
| 63 | + raise ValueError(f'kv must have shape {(num_tokens, (hc_mult + 1) * hidden_size)}, got {tuple(kv.shape)}.') | ||
| 64 | + if weight_fused.shape != (hc_mult, hidden_size): | ||
| 65 | + raise ValueError(f'weight_fused must have shape {(hc_mult, hidden_size)}, got {tuple(weight_fused.shape)}.') | ||
| 66 | + return num_tokens, hc_mult, hidden_size | ||
| 67 | + | ||
| 68 | + | ||
| 69 | +def _split_kv(kv: torch.Tensor, hc_mult: int, hidden_size: int) -> tuple[torch.Tensor, torch.Tensor]: | ||
| 70 | + num_tokens = kv.shape[0] | ||
| 71 | + k, v = kv.split([hc_mult * hidden_size, hidden_size], dim=-1) | ||
| 72 | + return k.view(num_tokens, hc_mult, hidden_size), v | ||
| 73 | + | ||
| 74 | + | ||
| 75 | +def engram_gate_fwd( | ||
| 76 | + hidden_states: torch.Tensor, | ||
| 77 | + kv: torch.Tensor, | ||
| 78 | + weight_fused: torch.Tensor, | ||
| 79 | + eps: float, | ||
| 80 | + clamp_value: float, | ||
| 81 | + save_for_backward: bool = True, | ||
| 82 | +) -> tuple[ | ||
| 83 | + torch.Tensor, | ||
| 84 | + torch.Tensor | None, | ||
| 85 | + torch.Tensor | None, | ||
| 86 | + torch.Tensor | None, | ||
| 87 | + torch.Tensor | None, | ||
| 88 | +]: | ||
| 89 | + """Apply the Engram gate and optionally return backward statistics. | ||
| 90 | + | ||
| 91 | + ``hidden_states`` has shape ``(num_tokens, 4, hidden_size)``. ``kv`` packs | ||
| 92 | + four key heads followed by one shared value head and therefore has shape | ||
| 93 | + ``(num_tokens, 5 * hidden_size)``. All activation tensors use BF16; | ||
| 94 | + ``weight_fused`` and the saved statistics use FP32. | ||
| 95 | + """ | ||
| 96 | + | ||
| 97 | + num_tokens, hc_mult, hidden_size = _validate_common_inputs(hidden_states, kv, weight_fused) | ||
| 98 | + _check_real('eps', eps, positive=False) | ||
| 99 | + _check_real('clamp_value', clamp_value, positive=True) | ||
| 100 | + if not isinstance(save_for_backward, bool): | ||
| 101 | + raise TypeError('save_for_backward must be bool.') | ||
| 102 | + | ||
| 103 | + output = torch.empty_like(hidden_states) | ||
| 104 | + stats_shape = (num_tokens, hc_mult) | ||
| 105 | + if save_for_backward: | ||
| 106 | + dot = torch.empty(stats_shape, dtype=torch.float32, device=hidden_states.device) | ||
| 107 | + gate_score = torch.empty_like(dot) | ||
| 108 | + rstd_x = torch.empty_like(dot) | ||
| 109 | + rstd_k = torch.empty_like(dot) | ||
| 110 | + else: | ||
| 111 | + # The no-save Ascend kernel keeps a signature-compatible placeholder | ||
| 112 | + # but does not read or write any of these tensors. | ||
| 113 | + dot = torch.empty(stats_shape, dtype=torch.float32, device=hidden_states.device) | ||
| 114 | + gate_score = rstd_x = rstd_k = dot | ||
| 115 | + | ||
| 116 | + if num_tokens == 0: | ||
| 117 | + if save_for_backward: | ||
| 118 | + return output, dot, gate_score, rstd_x, rstd_k | ||
| 119 | + return output, None, None, None, None | ||
| 120 | + | ||
| 121 | + k, v = _split_kv(kv, hc_mult, hidden_size) | ||
| 122 | + k_stride_s, k_stride_h, _ = k.stride() | ||
| 123 | + v_stride_s, _ = v.stride() | ||
| 124 | + num_cores = get_num_vector_cores() | ||
| 125 | + if num_cores % hc_mult: | ||
| 126 | + raise RuntimeError(f'Ascend vector core count {num_cores} must be divisible by hc_mult={hc_mult}.') | ||
| 127 | + kernel = get_engram_gate_fwd_kernel( | ||
| 128 | + hidden_size, | ||
| 129 | + float(eps), | ||
| 130 | + hidden_size**-0.5, | ||
| 131 | + k_stride_s, | ||
| 132 | + k_stride_h, | ||
| 133 | + v_stride_s, | ||
| 134 | + float(clamp_value), | ||
| 135 | + hc_mult, | ||
| 136 | + num_cores, | ||
| 137 | + save_for_backward, | ||
| 138 | + ) | ||
| 139 | + if should_print_kernel_source(): | ||
| 140 | + print(kernel.get_kernel_source()) | ||
| 141 | + kernel(hidden_states, k, v, weight_fused, output, dot, gate_score, rstd_x, rstd_k) | ||
| 142 | + | ||
| 143 | + if save_for_backward: | ||
| 144 | + return output, dot, gate_score, rstd_x, rstd_k | ||
| 145 | + return output, None, None, None, None | ||
| 146 | + | ||
| 147 | + | ||
| 148 | +def engram_gate_bwd( | ||
| 149 | + grad_out: torch.Tensor, | ||
| 150 | + hidden_states: torch.Tensor, | ||
| 151 | + kv: torch.Tensor, | ||
| 152 | + weight_fused: torch.Tensor, | ||
| 153 | + dot: torch.Tensor, | ||
| 154 | + gate_score: torch.Tensor, | ||
| 155 | + rstd_x: torch.Tensor, | ||
| 156 | + rstd_k: torch.Tensor, | ||
| 157 | + clamp_value: float, | ||
| 158 | +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | ||
| 159 | + """Run the Engram gate backward kernel. | ||
| 160 | + | ||
| 161 | + Returns gradients for ``hidden_states`` and packed ``kv``, plus partial | ||
| 162 | + FP32 gradients for ``weight_fused``. Pass the latter to | ||
| 163 | + :func:`engram_grad_w_reduce` together with the unfused weights. | ||
| 164 | + """ | ||
| 165 | + | ||
| 166 | + num_tokens, hc_mult, hidden_size = _validate_common_inputs(hidden_states, kv, weight_fused) | ||
| 167 | + _check_npu_tensor('grad_out', grad_out, dtype=torch.bfloat16, device=hidden_states.device) | ||
| 168 | + if grad_out.shape != hidden_states.shape: | ||
| 169 | + raise ValueError(f'grad_out must have shape {tuple(hidden_states.shape)}, got {tuple(grad_out.shape)}.') | ||
| 170 | + stats_shape = (num_tokens, hc_mult) | ||
| 171 | + for name, tensor in ( | ||
| 172 | + ('dot', dot), | ||
| 173 | + ('gate_score', gate_score), | ||
| 174 | + ('rstd_x', rstd_x), | ||
| 175 | + ('rstd_k', rstd_k), | ||
| 176 | + ): | ||
| 177 | + _check_npu_tensor(name, tensor, dtype=torch.float32, device=hidden_states.device) | ||
| 178 | + if tensor.shape != stats_shape: | ||
| 179 | + raise ValueError(f'{name} must have shape {stats_shape}, got {tuple(tensor.shape)}.') | ||
| 180 | + _check_real('clamp_value', clamp_value, positive=True) | ||
| 181 | + | ||
| 182 | + num_cores = get_num_vector_cores() | ||
| 183 | + num_main_cores = num_cores - _NUM_GRAD_V_CORES | ||
| 184 | + num_head_groups = hc_mult // 2 | ||
| 185 | + if num_main_cores <= 0 or num_main_cores % num_head_groups: | ||
| 186 | + raise RuntimeError( | ||
| 187 | + f'Ascend vector core count {num_cores} is incompatible with ' | ||
| 188 | + f'{_NUM_GRAD_V_CORES} grad-v cores and hc_mult={hc_mult}.' | ||
| 189 | + ) | ||
| 190 | + num_partials = num_main_cores // num_head_groups | ||
| 191 | + | ||
| 192 | + grad_hidden_states = torch.empty_like(hidden_states) | ||
| 193 | + grad_kv = torch.empty_like(kv) | ||
| 194 | + grad_w_partial = torch.empty( | ||
| 195 | + (num_partials, hc_mult, hidden_size), | ||
| 196 | + dtype=torch.float32, | ||
| 197 | + device=hidden_states.device, | ||
| 198 | + ) | ||
| 199 | + if num_tokens == 0: | ||
| 200 | + grad_w_partial.zero_() | ||
| 201 | + return grad_hidden_states, grad_kv, grad_w_partial | ||
| 202 | + | ||
| 203 | + k, v = _split_kv(kv, hc_mult, hidden_size) | ||
| 204 | + grad_k_flat, grad_v = grad_kv.split([hc_mult * hidden_size, hidden_size], dim=-1) | ||
| 205 | + grad_k = grad_k_flat.view(num_tokens, hc_mult, hidden_size) | ||
| 206 | + k_stride_s, k_stride_h, _ = k.stride() | ||
| 207 | + v_stride_s, _ = v.stride() | ||
| 208 | + kernel = get_engram_gate_bwd_kernel( | ||
| 209 | + hidden_size, | ||
| 210 | + hidden_size**-0.5, | ||
| 211 | + k_stride_s, | ||
| 212 | + k_stride_h, | ||
| 213 | + v_stride_s, | ||
| 214 | + num_cores, | ||
| 215 | + _NUM_GRAD_V_CORES, | ||
| 216 | + float(clamp_value), | ||
| 217 | + hc_mult, | ||
| 218 | + ) | ||
| 219 | + if should_print_kernel_source(): | ||
| 220 | + print(kernel.get_kernel_source()) | ||
| 221 | + kernel( | ||
| 222 | + grad_out, | ||
| 223 | + hidden_states, | ||
| 224 | + k, | ||
| 225 | + v, | ||
| 226 | + weight_fused, | ||
| 227 | + dot, | ||
| 228 | + gate_score, | ||
| 229 | + rstd_x, | ||
| 230 | + rstd_k, | ||
| 231 | + grad_hidden_states, | ||
| 232 | + grad_k, | ||
| 233 | + grad_v, | ||
| 234 | + grad_w_partial, | ||
| 235 | + ) | ||
| 236 | + return grad_hidden_states, grad_kv, grad_w_partial | ||
| @@ -0,0 +1,555 @@ | |||
| 1 | +"""TileLang Ascend implementation of the Engram gate forward/backward operators. | ||
| 2 | + | ||
| 3 | +The implementation follows the mathematical definition of the CUDA operator, | ||
| 4 | +but uses Ascend's GM -> UB vector execution model. Forward uses SimdVF while | ||
| 5 | +backward is deliberately split into three SimtVF kernels so every kernel has a | ||
| 6 | +small, regular UB working set. | ||
| 7 | +""" | ||
| 8 | + | ||
| 9 | +import tilelang | ||
| 10 | +from tilelang import language as T | ||
| 11 | +from tilelang.language import simd as S | ||
| 12 | + | ||
| 13 | +_VEC = 64 # fp32 lanes in one 2048-bit Ascend vector register | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +@tilelang.jit | ||
| 17 | +def get_engram_gate_fwd_kernel( | ||
| 18 | + hidden_size: int, | ||
| 19 | + eps: float, | ||
| 20 | + scalar: float, | ||
| 21 | + k_stride_s: int, | ||
| 22 | + k_stride_h: int, | ||
| 23 | + v_stride_s: int, | ||
| 24 | + clamp_value: float = 1e-6, | ||
| 25 | + hc_mult: int = 4, | ||
| 26 | + num_cores: int = 64, | ||
| 27 | + save_for_backward: bool = True, | ||
| 28 | +): | ||
| 29 | + """Build the Ascend forward kernel.""" | ||
| 30 | + num_tokens = T.dynamic('num_tokens') | ||
| 31 | + assert hidden_size % _VEC == 0, 'Ascend SIMD Engram gate requires hidden_size % 64 == 0' | ||
| 32 | + assert num_cores % hc_mult == 0, 'num_cores must be divisible by hc_mult' | ||
| 33 | + num_vectors = hidden_size // _VEC | ||
| 34 | + heads_per_task = 2 if hidden_size <= 6144 else 1 | ||
| 35 | + num_head_groups = hc_mult // heads_per_task | ||
| 36 | + # Deeper buffering hides MTE latency for shapes that leave enough UB room. | ||
| 37 | + if heads_per_task == 2: | ||
| 38 | + num_stages = 6 if hidden_size <= 2560 else (5 if hidden_size <= 3072 else (3 if hidden_size <= 4096 else 2)) | ||
| 39 | + v_stages = num_stages | ||
| 40 | + else: | ||
| 41 | + num_stages = 6 if hidden_size <= 3072 else (5 if hidden_size <= 4096 else (2 if hidden_size < 6144 else 3)) | ||
| 42 | + v_stages = 2 if hidden_size >= 6144 else num_stages | ||
| 43 | + | ||
| 44 | + @T.prim_func | ||
| 45 | + def kernel( | ||
| 46 | + hidden_states: T.Tensor[(num_tokens, hc_mult, hidden_size), T.bfloat16], | ||
| 47 | + k: T.StridedTensor[ | ||
| 48 | + (num_tokens, hc_mult, hidden_size), | ||
| 49 | + (k_stride_s, k_stride_h, 1), | ||
| 50 | + T.bfloat16, | ||
| 51 | + ], | ||
| 52 | + v: T.StridedTensor[ | ||
| 53 | + (num_tokens, hidden_size), | ||
| 54 | + (v_stride_s, 1), | ||
| 55 | + T.bfloat16, | ||
| 56 | + ], | ||
| 57 | + weight_fused: T.Tensor[(hc_mult, hidden_size), T.float32], | ||
| 58 | + output: T.Tensor[(num_tokens, hc_mult, hidden_size), T.bfloat16], | ||
| 59 | + dot_out: T.Tensor[(num_tokens, hc_mult), T.float32], | ||
| 60 | + gate_out: T.Tensor[(num_tokens, hc_mult), T.float32], | ||
| 61 | + rstd_x_out: T.Tensor[(num_tokens, hc_mult), T.float32], | ||
| 62 | + rstd_k_out: T.Tensor[(num_tokens, hc_mult), T.float32], | ||
| 63 | + ) -> None: | ||
| 64 | + total_tasks = num_tokens * num_head_groups | ||
| 65 | + iterations = T.ceildiv(total_tasks, num_cores) | ||
| 66 | + | ||
| 67 | + with T.Kernel(num_cores) as core_id: | ||
| 68 | + x_ub = T.alloc_shared((heads_per_task, hidden_size), T.bfloat16) | ||
| 69 | + k_ub = T.alloc_shared((heads_per_task, hidden_size), T.bfloat16) | ||
| 70 | + v_ub = T.alloc_shared((hidden_size,), T.bfloat16) | ||
| 71 | + w_ub = T.alloc_shared((heads_per_task, hidden_size), T.float32) | ||
| 72 | + out_ub = T.alloc_shared((heads_per_task, hidden_size), T.bfloat16) | ||
| 73 | + dot_ub = T.alloc_shared((8,), T.float32) | ||
| 74 | + gate_ub = T.alloc_shared((8,), T.float32) | ||
| 75 | + rstd_x_ub = T.alloc_shared((8,), T.float32) | ||
| 76 | + rstd_k_ub = T.alloc_shared((8,), T.float32) | ||
| 77 | + T.annotate_buffer_versions( | ||
| 78 | + { | ||
| 79 | + x_ub: num_stages, | ||
| 80 | + k_ub: num_stages, | ||
| 81 | + v_ub: v_stages, | ||
| 82 | + out_ub: num_stages, | ||
| 83 | + dot_ub: num_stages, | ||
| 84 | + gate_ub: num_stages, | ||
| 85 | + rstd_x_ub: num_stages, | ||
| 86 | + rstd_k_ub: num_stages, | ||
| 87 | + } | ||
| 88 | + ) | ||
| 89 | + | ||
| 90 | + # Persistent cores keep one adjacent head group and its weights in | ||
| 91 | + # UB. Grouped heads share the token's v load and scalar DMAs. | ||
| 92 | + head_group = core_id % num_head_groups | ||
| 93 | + head_base = head_group * heads_per_task | ||
| 94 | + T.copy( | ||
| 95 | + weight_fused[head_base : head_base + heads_per_task, :], | ||
| 96 | + w_ub[:heads_per_task, :hidden_size], | ||
| 97 | + ) | ||
| 98 | + | ||
| 99 | + for task_iter in T.Pipelined( | ||
| 100 | + iterations, | ||
| @@ -0,0 +1,83 @@ | |||
| 1 | +"""Public interface for reducing Engram fused-weight gradients.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import torch | ||
| 6 | + | ||
| 7 | +from cann_ops_tilelang.config import require_ascend, should_print_kernel_source | ||
| 8 | + | ||
| 9 | +from .engram_grad_w_reduce_kernel import get_engram_grad_w_reduce_kernel | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +def _check_tensor( | ||
| 13 | + name: str, | ||
| 14 | + tensor: torch.Tensor, | ||
| 15 | + *, | ||
| 16 | + dtype: torch.dtype, | ||
| 17 | + device: torch.device | None = None, | ||
| 18 | +) -> None: | ||
| 19 | + if not isinstance(tensor, torch.Tensor): | ||
| 20 | + raise TypeError(f'{name} must be a torch.Tensor.') | ||
| 21 | + if tensor.device.type != 'npu': | ||
| 22 | + raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.') | ||
| 23 | + if device is not None and tensor.device != device: | ||
| 24 | + raise ValueError(f'{name} must be on {device}, got {tensor.device}.') | ||
| 25 | + if tensor.dtype != dtype: | ||
| 26 | + raise TypeError(f'{name} must use {dtype}, got {tensor.dtype}.') | ||
| 27 | + if not tensor.is_contiguous(): | ||
| 28 | + raise ValueError(f'{name} must be contiguous.') | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def engram_grad_w_reduce( | ||
| 32 | + grad_w_partial: torch.Tensor, | ||
| 33 | + weight_hidden: torch.Tensor, | ||
| 34 | + weight_embed: torch.Tensor, | ||
| 35 | + grad_weight_hidden: torch.Tensor, | ||
| 36 | + grad_weight_embed: torch.Tensor, | ||
| 37 | +) -> None: | ||
| 38 | + """Accumulate partial fused-weight gradients into both unfused weights. | ||
| 39 | + | ||
| 40 | + ``grad_weight_hidden`` and ``grad_weight_embed`` are FP32 accumulation | ||
| 41 | + buffers and are updated in place. The function returns ``None``. | ||
| 42 | + """ | ||
| 43 | + | ||
| 44 | + require_ascend() | ||
| 45 | + _check_tensor('grad_w_partial', grad_w_partial, dtype=torch.float32) | ||
| 46 | + _check_tensor('weight_hidden', weight_hidden, dtype=torch.bfloat16, device=grad_w_partial.device) | ||
| 47 | + _check_tensor('weight_embed', weight_embed, dtype=torch.bfloat16, device=grad_w_partial.device) | ||
| 48 | + _check_tensor( | ||
| 49 | + 'grad_weight_hidden', | ||
| 50 | + grad_weight_hidden, | ||
| 51 | + dtype=torch.float32, | ||
| 52 | + device=grad_w_partial.device, | ||
| 53 | + ) | ||
| 54 | + _check_tensor( | ||
| 55 | + 'grad_weight_embed', | ||
| 56 | + grad_weight_embed, | ||
| 57 | + dtype=torch.float32, | ||
| 58 | + device=grad_w_partial.device, | ||
| 59 | + ) | ||
| 60 | + | ||
| 61 | + if grad_w_partial.ndim != 3: | ||
| 62 | + raise ValueError('grad_w_partial must have shape (num_partials, hc_mult, hidden_size).') | ||
| 63 | + num_partials, hc_mult, hidden_size = grad_w_partial.shape | ||
| 64 | + if num_partials < 1: | ||
| 65 | + raise ValueError('grad_w_partial must contain at least one partial gradient.') | ||
| 66 | + if hc_mult != 4: | ||
| 67 | + raise ValueError(f'Engram gradient reduction currently requires hc_mult == 4, got {hc_mult}.') | ||
| 68 | + if hidden_size == 0 or hidden_size % 256: | ||
| 69 | + raise ValueError(f'hidden_size must be a positive multiple of 256, got {hidden_size}.') | ||
| 70 | + expected_shape = (hc_mult, hidden_size) | ||
| 71 | + for name, tensor in ( | ||
| 72 | + ('weight_hidden', weight_hidden), | ||
| 73 | + ('weight_embed', weight_embed), | ||
| 74 | + ('grad_weight_hidden', grad_weight_hidden), | ||
| 75 | + ('grad_weight_embed', grad_weight_embed), | ||
| 76 | + ): | ||
| 77 | + if tensor.shape != expected_shape: | ||
| 78 | + raise ValueError(f'{name} must have shape {expected_shape}, got {tuple(tensor.shape)}.') | ||
| 79 | + | ||
| 80 | + kernel = get_engram_grad_w_reduce_kernel(hidden_size, num_partials, hc_mult) | ||
| 81 | + if should_print_kernel_source(): | ||
| 82 | + print(kernel.get_kernel_source()) | ||
| 83 | + kernel(grad_w_partial, weight_hidden, weight_embed, grad_weight_hidden, grad_weight_embed) | ||
| @@ -0,0 +1,126 @@ | |||
| 1 | +import tilelang | ||
| 2 | +from tilelang import language as T | ||
| 3 | +from tilelang.language import simd as S | ||
| 4 | + | ||
| 5 | +from cann_ops_tilelang.config import get_num_vector_cores | ||
| 6 | + | ||
| 7 | +_VEC = 64 | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +def _choose_num_batches(num_rows: int, max_rows_per_batch: int) -> int: | ||
| 11 | + if num_rows == 1: | ||
| 12 | + return 1 | ||
| 13 | + min_batches = (num_rows + max_rows_per_batch - 1) // max_rows_per_batch | ||
| 14 | + if min_batches == 1: | ||
| 15 | + for candidate in range(2, min(4, num_rows) + 1): | ||
| 16 | + if num_rows % candidate == 0: | ||
| 17 | + return candidate | ||
| 18 | + return num_rows | ||
| 19 | + for candidate in range(min_batches, num_rows + 1): | ||
| 20 | + if num_rows % candidate == 0: | ||
| 21 | + return candidate | ||
| 22 | + return num_rows | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +@tilelang.jit | ||
| 26 | +def get_engram_grad_w_reduce_kernel(hidden_size: int, num_persistent_blocks: int, hc_mult: int = 4): | ||
| 27 | + assert num_persistent_blocks > 0 | ||
| 28 | + | ||
| 29 | + blk_d = 256 if hidden_size <= 3072 or hidden_size == 6144 and num_persistent_blocks > 74 else 512 | ||
| 30 | + assert hidden_size % blk_d == 0 | ||
| 31 | + | ||
| 32 | + max_rows_per_batch = 74 if hidden_size > 3072 and blk_d == 256 else 37 | ||
| 33 | + num_row_batches = _choose_num_batches(num_persistent_blocks, max_rows_per_batch) | ||
| 34 | + rows_per_batch = num_persistent_blocks // num_row_batches | ||
| 35 | + use_explicit_unroll = rows_per_batch <= 14 | ||
| 36 | + use_two_accumulators = hidden_size <= 2560 and rows_per_batch >= 32 | ||
| 37 | + buffer_versions = 2 if num_row_batches > 1 else 1 | ||
| 38 | + row_pipeline_stages = 2 if num_row_batches > 1 else 0 | ||
| 39 | + num_tiles = hidden_size // blk_d | ||
| 40 | + num_chunks = blk_d // _VEC | ||
| 41 | + num_cores = get_num_vector_cores() | ||
| 42 | + persistent_stages = 3 | ||
| 43 | + | ||
| 44 | + @T.prim_func | ||
| 45 | + def engram_grad_w_reduce_kernel( | ||
| 46 | + grad_w_partial: T.Tensor[(num_persistent_blocks, hc_mult, hidden_size), T.float], | ||
| 47 | + weight_hidden: T.Tensor[(hc_mult, hidden_size), T.bfloat16], | ||
| 48 | + weight_embed: T.Tensor[(hc_mult, hidden_size), T.bfloat16], | ||
| 49 | + grad_weight_hidden: T.Tensor[(hc_mult, hidden_size), T.float], | ||
| 50 | + grad_weight_embed: T.Tensor[(hc_mult, hidden_size), T.float], | ||
| 51 | + ): | ||
| 52 | + with T.Kernel(num_cores) as core_id: | ||
| 53 | + wh_ub = T.alloc_shared((blk_d,), T.bfloat16) | ||
| 54 | + we_ub = T.alloc_shared((blk_d,), T.bfloat16) | ||
| 55 | + grad_wh_ub = T.alloc_shared((blk_d,), T.float) | ||
| 56 | + grad_we_ub = T.alloc_shared((blk_d,), T.float) | ||
| 57 | + grad_w_batch_ub = T.alloc_shared((rows_per_batch, blk_d), T.float) | ||
| 58 | + grad_w_acc_ub = T.alloc_shared((blk_d,), T.float) | ||
| 59 | + T.annotate_buffer_versions( | ||
| 60 | + { | ||
| 61 | + wh_ub: persistent_stages, | ||
| 62 | + we_ub: persistent_stages, | ||
| 63 | + grad_wh_ub: persistent_stages, | ||
| 64 | + grad_we_ub: persistent_stages, | ||
| 65 | + grad_w_batch_ub: buffer_versions, | ||
| 66 | + } | ||
| 67 | + ) | ||
| 68 | + | ||
| 69 | + for pid_h, pid_b in T.Persistent( | ||
| 70 | + [hc_mult, num_tiles], num_cores, core_id, group_size=1, num_stages=persistent_stages | ||
| 71 | + ): | ||
| 72 | + col_start = pid_b * blk_d | ||
| 73 | + col_end = col_start + blk_d | ||
| 74 | + T.copy(weight_hidden[pid_h, col_start:col_end], wh_ub) | ||
| 75 | + T.copy(weight_embed[pid_h, col_start:col_end], we_ub) | ||
| 76 | + T.copy(grad_weight_hidden[pid_h, col_start:col_end], grad_wh_ub) | ||
| 77 | + T.copy(grad_weight_embed[pid_h, col_start:col_end], grad_we_ub) | ||
| 78 | + | ||
| 79 | + with T.SimdVF(): | ||
| 80 | + for c in range(num_chunks): | ||
| 81 | + col = c * _VEC | ||
| 82 | + S.vsts(grad_w_acc_ub[col], S.vdup(0.0, T.float32)) | ||
| 83 | + | ||
| 84 | + for batch in T.Pipelined(num_row_batches, num_stages=row_pipeline_stages): | ||
| 85 | + row_start = batch * rows_per_batch | ||
| 86 | + T.copy( | ||
| 87 | + grad_w_partial[row_start : row_start + rows_per_batch, pid_h, col_start:col_end], | ||
| 88 | + grad_w_batch_ub, | ||
| 89 | + l2_cache_ctrl='NOTALLOC_KEEP', | ||
| 90 | + ) | ||
| 91 | + with T.SimdVF(): | ||
| 92 | + acc0 = S.alloc_var(T.float32) | ||
| 93 | + acc1 = S.alloc_var(T.float32) | ||
| 94 | + for c in range(num_chunks): | ||
| 95 | + col = c * _VEC | ||
| 96 | + acc0 = S.vld(grad_w_acc_ub[col]) | ||
| 97 | + if use_explicit_unroll: | ||
| 98 | + for row in T.unroll(rows_per_batch, explicit=True): | ||
| 99 | + acc0 = S.vadd(acc0, S.vld(grad_w_batch_ub[row, col])) | ||
| 100 | + elif use_two_accumulators: | ||
| 101 | + acc1 = S.vdup(0.0, T.float32) | ||
| 102 | + for pair in T.serial(rows_per_batch // 2): | ||
| 103 | + row0 = pair * 2 | ||
| 104 | + acc0 = S.vadd(acc0, S.vld(grad_w_batch_ub[row0, col])) | ||
| 105 | + acc1 = S.vadd(acc1, S.vld(grad_w_batch_ub[row0 + 1, col])) | ||
| 106 | + if rows_per_batch % 2: | ||
| 107 | + acc0 = S.vadd(acc0, S.vld(grad_w_batch_ub[rows_per_batch - 1, col])) | ||
| 108 | + acc0 = S.vadd(acc0, acc1) | ||
| 109 | + else: | ||
| 110 | + for row in T.serial(rows_per_batch): | ||
| 111 | + acc0 = S.vadd(acc0, S.vld(grad_w_batch_ub[row, col])) | ||
| 112 | + S.vsts(grad_w_acc_ub[col], acc0) | ||
| 113 | + | ||
| 114 | + with T.SimdVF(): | ||
| 115 | + for c in range(num_chunks): | ||
| 116 | + col = c * _VEC | ||
| 117 | + total = S.vld(grad_w_acc_ub[col]) | ||
| 118 | + wh = S.vcvt(S.vld(wh_ub[col], dist='UNPK_B16'), T.float32, part=0) | ||
| 119 | + we = S.vcvt(S.vld(we_ub[col], dist='UNPK_B16'), T.float32, part=0) | ||
| 120 | + S.vsts(grad_wh_ub[col], S.vadd(S.vld(grad_wh_ub[col]), S.vmul(total, we))) | ||
| 121 | + S.vsts(grad_we_ub[col], S.vadd(S.vld(grad_we_ub[col]), S.vmul(total, wh))) | ||
| 122 | + | ||
| 123 | + T.copy(grad_wh_ub, grad_weight_hidden[pid_h, col_start:col_end]) | ||
| 124 | + T.copy(grad_we_ub, grad_weight_embed[pid_h, col_start:col_end]) | ||
| 125 | + | ||
| 126 | + return engram_grad_w_reduce_kernel | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | +"""Small dependency-free helpers shared by operator wrappers.""" | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +def ceil_div(value: int, divisor: int) -> int: | ||
| 5 | + if divisor <= 0: | ||
| 6 | + raise ValueError(f'divisor must be positive, got {divisor}.') | ||
| 7 | + return (value + divisor - 1) // divisor | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +def align_up(value: int, alignment: int) -> int: | ||
| 11 | + return ceil_div(value, alignment) * alignment | ||
| @@ -0,0 +1,76 @@ | |||
| 1 | +# ęµčÆäøę§č½åå½ę”ę¶ | ||
| 2 | + | ||
| 3 | +ęµčÆę”ę¶ę²æēØ `TileKernels-Nightly` ēå®ę“é¾č·Æļ¼å¹¶ę¶ęäøŗ Ascend/NPU čÆä¹ćę ¹ `conftest.py` č“č“£åØ TileLang åÆ¼å „åē»å® xdist workerļ¼éåč£ č½½ä»„äøē¬ē« pytest ęä»¶ļ¼ | ||
| 4 | + | ||
| 5 | +- `pytest_random_plugin.py`ļ¼`--seed` äø node ID ē»åļ¼å¾å°éēØä¾ēسå®éęŗē§åļ¼ | ||
| 6 | +- `pytest_npu_mem_plugin.py`ļ¼č®°å½å³°å¼ NPU ę¾åļ¼čŖåØę č®° `large_npu_mem`ļ¼ | ||
| 7 | +- `pytest_benchmark_plugin.py`ļ¼č®”ę¶ćJSONL č¾åŗćę§č½åŗēŗæęÆč¾äøå·ę°ļ¼ | ||
| 8 | +- `pytest_improvement_plugin.py`ļ¼éčæ `--legacy-table` ę±ę»å ä½å¹³åę§č½ļ¼ | ||
| 9 | +- `pytest_xdist_failfast_plugin.py`ļ¼å¹¶č” `-x` ę¶åę¢å ØéØ workerļ¼ | ||
| 10 | +- `pytest_tilelang_warning_plugin.py`ļ¼åØē»ē«Æę±ę» TileLang ē¼čÆč¦åć | ||
| 11 | + | ||
| 12 | +## ęµčÆåēŗ§ | ||
| 13 | + | ||
| 14 | +`OPS_TILELANG_TEST_LEVEL` åå¼äøŗ `0`ć`1`ć`2`ļ¼åå«č”Øē¤ŗę øåæćé»č®¤åå®ę“åę°éåćåę°ēęåØä½äŗ `tests/testing/generator.py`ć | ||
| 15 | + | ||
| 16 | +```bash | ||
| 17 | +OPS_TILELANG_TEST_LEVEL=0 pytest tests/engram -m 'not benchmark' | ||
| 18 | +OPS_TILELANG_TEST_LEVEL=2 pytest tests/engram -m 'not benchmark' | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +## ę§č½åŗēŗæ | ||
| 22 | + | ||
| 23 | +benchmark é»č®¤č·³čæļ¼åæ é”»ę¾å¼ä¼ å „ `--run-benchmark`ćAscend ä½æēØ `msprof` å端ļ¼å¹¶å°ē»ęå½å „ `a5` é群ćåŗēŗæååØåØ `tests/<operator>/benchmark/<ęµčÆęä»¶>.jsonl`ć | ||
| 24 | + | ||
| 25 | +```bash | ||
| 26 | +# å ØéØ benchmark | ||
| 27 | +pytest -m benchmark --run-benchmark | ||
| 28 | + | ||
| 29 | +# ēØåę°č”Øč¾¾å¼ēéļ¼ęÆę and/or/notćę¬å·åę£å | ||
| 30 | +pytest -m benchmark --run-benchmark -K 'hidden=4096 or hidden=7168' | ||
| 31 | + | ||
| 32 | +# ęå®č¾åŗćéå¼åęå°ē»åƹåå | ||
| 33 | +pytest -m benchmark --run-benchmark \ | ||
| 34 | + --benchmark-output=/tmp/ops-tilelang.jsonl \ | ||
| 35 | + --benchmark-regression-threshold=0.10 \ | ||
| 36 | + --benchmark-min-delta-us=1.0 | ||
| 37 | + | ||
| 38 | +# 甮认ę§č½ååęÆé¢ęåå·ę°åēåŗēŗæ | ||
| 39 | +pytest tests/engram/test_engram_fused_weight.py \ | ||
| 40 | + -m benchmark --run-benchmark --refresh-baseline | ||
| 41 | + | ||
| 42 | +# Engram gate åå/ååå梯度å½å¹¶ | ||
| 43 | +pytest tests/engram -m benchmark --run-benchmark --refresh-baseline | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +`--benchmark-verbose` å±ē¤ŗę©å±åļ¼`--legacy-table` å±ē¤ŗē®åēŗ§å ä½å¹³åę±ę»ć | ||
| 47 | + | ||
| 48 | +## NPU ę¾åē»å | ||
| 49 | + | ||
| 50 | +ę¾åē»åååØåØ `tests/<operator>/npu_mem_profiles/<ęµčÆęä»¶>.json`ćę ē»åēęµčÆä¼å å½å „ `large_npu_mem`ļ¼éæå 仄é«å¹¶åčæč”ęŖē„ēØä¾ć | ||
| 51 | + | ||
| 52 | +```bash | ||
| 53 | +# čæč”å¹¶å·ę°éäøēØä¾ēę¾åē»å | ||
| 54 | +pytest tests/engram/test_engram_fused_weight.py -m 'not benchmark' --refresh-mem | ||
| 55 | + | ||
| 56 | +# å·ę°ę“äøŖ Engram ē®åęēē»å | ||
| 57 | +pytest tests/engram -m 'not benchmark' --refresh-mem | ||
| 58 | + | ||
| 59 | +# åŖčæč”å°ę ē»åēęµčÆ | ||
| 60 | +pytest --refresh-mem --refresh-mem-missing-only | ||
| 61 | + | ||
| 62 | +# č°ę“大ę¾åéå¼ęäæēåå§ JSONL | ||
| 63 | +pytest --refresh-mem --npu-mem-threshold=15000 \ | ||
| 64 | + --npu-mem-output=/tmp/ops-tilelang-mem.jsonl | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +`--npu-mem-flush-interval` ę§å¶éæä»»å”åØęååé“éć | ||
| 68 | + | ||
| 69 | +## ę åę§č”å „å£ | ||
| 70 | + | ||
| 71 | +`scripts/run_test.sh` é»č®¤åäø¤é¶ę®µčæč”ļ¼ę®éęµčÆęÆå¼ NPU 两个 workerļ¼å¤§/ęŖē„ę¾åęµčÆęÆå¼ NPU äøäøŖ workerćåÆéčæ `TILELANG_TEST_WORKERS_PER_NPU` å `TILELANG_TEST_WORKERS_PER_NPU_LARGE` č°ę“ć | ||
| 72 | + | ||
| 73 | +```bash | ||
| 74 | +./scripts/run_test.sh | ||
| 75 | +OPS_TILELANG_RUN_BENCHMARK=1 ./scripts/run_test.sh tests/engram | ||
| 76 | +``` | ||
| @@ -0,0 +1,91 @@ | |||
| 1 | +"""Root pytest configuration and Ascend xdist worker binding.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import os | ||
| 6 | +import signal | ||
| 7 | +import subprocess | ||
| 8 | +import sys | ||
| 9 | +from pathlib import Path | ||
| 10 | + | ||
| 11 | +import pytest | ||
| 12 | + | ||
| 13 | +_NPU_SMI_PATH = '/usr/local/bin/npu-smi' | ||
| 14 | + | ||
| 15 | +# Source checkouts work without an editable install. This must precede plugin | ||
| 16 | +# imports because the plugins import cann_ops_tilelang during pytest startup. | ||
| 17 | +_REPO_ROOT = Path(__file__).resolve().parents[1] | ||
| 18 | +_SRC_DIR = _REPO_ROOT / 'src' | ||
| 19 | +if str(_SRC_DIR) not in sys.path: | ||
| 20 | + sys.path.insert(0, str(_SRC_DIR)) | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +def _ignore_sigint_in_xdist_workers() -> None: | ||
| 24 | + """Leave Ctrl+C handling to the xdist controller process.""" | ||
| 25 | + | ||
| 26 | + if os.environ.get('PYTEST_XDIST_WORKER') is not None: | ||
| 27 | + signal.signal(signal.SIGINT, signal.SIG_IGN) | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +def _visible_npus() -> list[str]: | ||
| 31 | + visible = os.environ.get('ASCEND_RT_VISIBLE_DEVICES') | ||
| 32 | + if visible and visible.strip(): | ||
| 33 | + return [device.strip() for device in visible.split(',') if device.strip()] | ||
| 34 | + | ||
| 35 | + try: | ||
| 36 | + result = subprocess.run( | ||
| 37 | + [_NPU_SMI_PATH, 'info', '-l'], | ||
| 38 | + capture_output=True, | ||
| 39 | + check=False, | ||
| 40 | + text=True, | ||
| 41 | + timeout=10, | ||
| 42 | + ) | ||
| 43 | + except (FileNotFoundError, subprocess.TimeoutExpired): | ||
| 44 | + return ['0'] | ||
| 45 | + if result.returncode != 0: | ||
| 46 | + return ['0'] | ||
| 47 | + lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] | ||
| 48 | + return [str(index) for index in range(max(len(lines) - 1, 1))] | ||
| 49 | + | ||
| 50 | + | ||
| 51 | +def _bind_xdist_worker_to_npu() -> None: | ||
| 52 | + """Bind each xdist worker before TileLang can initialize the runtime.""" | ||
| 53 | + | ||
| 54 | + worker_id = os.environ.get('PYTEST_XDIST_WORKER') | ||
| 55 | + if worker_id is None or not os.path.exists('/dev/davinci_manager'): | ||
| 56 | + return | ||
| 57 | + worker_index = int(worker_id.removeprefix('gw')) | ||
| 58 | + devices = _visible_npus() | ||
| 59 | + os.environ['ASCEND_RT_VISIBLE_DEVICES'] = devices[worker_index % len(devices)] | ||
| 60 | + | ||
| 61 | + | ||
| 62 | +_ignore_sigint_in_xdist_workers() | ||
| 63 | +_bind_xdist_worker_to_npu() | ||
| 64 | + | ||
| 65 | + | ||
| 66 | +@pytest.fixture(autouse=True) | ||
| 67 | +def reset_cann_ops_tilelang_runtime_config(): | ||
| 68 | + """Prevent cached device configuration from leaking between tests.""" | ||
| 69 | + | ||
| 70 | + from cann_ops_tilelang.config import clear_runtime_caches | ||
| 71 | + | ||
| 72 | + clear_runtime_caches() | ||
| 73 | + yield | ||
| 74 | + clear_runtime_caches() | ||
| 75 | + | ||
| 76 | + | ||
| 77 | +@pytest.fixture(scope='session') | ||
| 78 | +def npu_available() -> bool: | ||
| 79 | + from cann_ops_tilelang.config import is_ascend_available | ||
| 80 | + | ||
| 81 | + return is_ascend_available() | ||
| 82 | + | ||
| 83 | + | ||
| 84 | +pytest_plugins = [ | ||
| 85 | + 'tests.pytest_improvement_plugin', | ||
| 86 | + 'tests.pytest_random_plugin', | ||
| 87 | + 'tests.pytest_xdist_failfast_plugin', | ||
| 88 | + 'tests.pytest_npu_mem_plugin', | ||
| 89 | + 'tests.pytest_benchmark_plugin', | ||
| 90 | + 'tests.pytest_tilelang_warning_plugin', | ||
| 91 | +] | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 2048}, "param_id": "hc=4,hidden=2048,", "cluster": "a5", "time_us": 1.3, "bandwidth_gbs": 50.4918} | ||
| 2 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 2560}, "param_id": "hc=4,hidden=2560,", "cluster": "a5", "time_us": 1.34, "bandwidth_gbs": 61.0272} | ||
| 3 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 3072}, "param_id": "hc=4,hidden=3072,", "cluster": "a5", "time_us": 1.37, "bandwidth_gbs": 71.5441} | ||
| 4 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 4096}, "param_id": "hc=4,hidden=4096,", "cluster": "a5", "time_us": 1.35, "bandwidth_gbs": 96.766} | ||
| 5 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 6144}, "param_id": "hc=4,hidden=6144,", "cluster": "a5", "time_us": 1.39, "bandwidth_gbs": 141.5132} | ||
| 6 | +{"kernel": "engram_fused_weight", "operation": "fwd", "params": {"hc": 4, "hidden": 7168}, "param_id": "hc=4,hidden=7168,", "cluster": "a5", "time_us": 1.4, "bandwidth_gbs": 163.8539} | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 2048, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=2048,", "cluster": "a5", "time_us": 24.17, "bandwidth_gbs": 1952.3851} | ||
| 2 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 2560, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=2560,", "cluster": "a5", "time_us": 27.03, "bandwidth_gbs": 2181.4117} | ||
| 3 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 3072, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=3072,", "cluster": "a5", "time_us": 29.98, "bandwidth_gbs": 2360.3182} | ||
| 4 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 4096, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=4096,", "cluster": "a5", "time_us": 38.45, "bandwidth_gbs": 2453.6577} | ||
| 5 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 6144, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=6144,", "cluster": "a5", "time_us": 47.85, "bandwidth_gbs": 2957.252} | ||
| 6 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 7168, "num_tokens": 512}, "param_id": "num_tokens=512,hc=4,hidden=7168,", "cluster": "a5", "time_us": 53.51, "bandwidth_gbs": 3084.788} | ||
| 7 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 2048, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=2048,", "cluster": "a5", "time_us": 331.65, "bandwidth_gbs": 2178.5641} | ||
| 8 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 2560, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=2560,", "cluster": "a5", "time_us": 390.58, "bandwidth_gbs": 2312.0076} | ||
| 9 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 3072, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=3072,", "cluster": "a5", "time_us": 426.8, "bandwidth_gbs": 2538.7221} | ||
| 10 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 4096, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=4096,", "cluster": "a5", "time_us": 559.42, "bandwidth_gbs": 2582.1752} | ||
| 11 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 6144, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=6144,", "cluster": "a5", "time_us": 709.97, "bandwidth_gbs": 3051.5528} | ||
| 12 | +{"kernel": "engram_gate_bwd", "operation": "bwd", "params": {"hc": 4, "hidden": 7168, "num_tokens": 8001}, "param_id": "num_tokens=8001,hc=4,hidden=7168,", "cluster": "a5", "time_us": 790.63, "bandwidth_gbs": 3196.8274} | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 2048, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=2048,", "cluster": "a5", "time_us": 12.02, "extra_params": {"save": false}, "bandwidth_gbs": 2270.6319} | ||
| 2 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 2560, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=2560,", "cluster": "a5", "time_us": 13.51, "extra_params": {"save": false}, "bandwidth_gbs": 2525.1327} | ||
| 3 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 3072, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=3072,", "cluster": "a5", "time_us": 18.3, "extra_params": {"save": false}, "bandwidth_gbs": 2237.1283} | ||
| 4 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 4096, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=4096,", "cluster": "a5", "time_us": 19.52, "extra_params": {"save": false}, "bandwidth_gbs": 2796.2392} | ||
| 5 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 6144, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=6144,", "cluster": "a5", "time_us": 28.54, "extra_params": {"save": false}, "bandwidth_gbs": 2869.2376} | ||
| 6 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 7168, "num_tokens": 512, "save": false}, "param_id": "num_tokens=512,hc=4,hidden=7168,", "cluster": "a5", "time_us": 31.74, "extra_params": {"save": false}, "bandwidth_gbs": 3009.6244} | ||
| 7 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 2048, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=2048,", "cluster": "a5", "time_us": 132.29, "extra_params": {"save": false}, "bandwidth_gbs": 3220.661} | ||
| 8 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 2560, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=2560,", "cluster": "a5", "time_us": 163.07, "extra_params": {"save": false}, "bandwidth_gbs": 3266.0326} | ||
| 9 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 3072, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=3072,", "cluster": "a5", "time_us": 193.26, "extra_params": {"save": false}, "bandwidth_gbs": 3307.0191} | ||
| 10 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 4096, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=4096,", "cluster": "a5", "time_us": 251.23, "extra_params": {"save": false}, "bandwidth_gbs": 3391.8703} | ||
| 11 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 6144, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=6144,", "cluster": "a5", "time_us": 380.96, "extra_params": {"save": false}, "bandwidth_gbs": 3355.231} | ||
| 12 | +{"kernel": "engram_gate_fwd", "operation": "fwd", "params": {"hc": 4, "hidden": 7168, "num_tokens": 8001, "save": false}, "param_id": "num_tokens=8001,hc=4,hidden=7168,", "cluster": "a5", "time_us": 449.61, "extra_params": {"save": false}, "bandwidth_gbs": 3316.7529} | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 2048, "num_partials": 30}, "param_id": "hidden=2048,num_partials=30,", "cluster": "a5", "time_us": 2.15, "bandwidth_gbs": 503.2415} | ||
| 2 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 2560, "num_partials": 30}, "param_id": "hidden=2560,num_partials=30,", "cluster": "a5", "time_us": 2.33, "bandwidth_gbs": 581.0937} | ||
| 3 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 3072, "num_partials": 30}, "param_id": "hidden=3072,num_partials=30,", "cluster": "a5", "time_us": 2.33, "bandwidth_gbs": 695.5042} | ||
| 4 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 4096, "num_partials": 30}, "param_id": "hidden=4096,num_partials=30,", "cluster": "a5", "time_us": 2.58, "bandwidth_gbs": 836.9596} | ||
| 5 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 6144, "num_partials": 30}, "param_id": "hidden=6144,num_partials=30,", "cluster": "a5", "time_us": 2.83, "bandwidth_gbs": 1148.1818} | ||
| 6 | +{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 7168, "num_partials": 30}, "param_id": "hidden=7168,num_partials=30,", "cluster": "a5", "time_us": 2.87, "bandwidth_gbs": 1319.1494} | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | +"""Pure PyTorch reference for the Engram gate operator.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import torch | ||
| 6 | + | ||
| 7 | + | ||
| 8 | +def engram_gate_ref( | ||
| 9 | + hidden_states: torch.Tensor, | ||
| 10 | + kv: torch.Tensor, | ||
| 11 | + weight_hidden: torch.Tensor, | ||
| 12 | + weight_embed: torch.Tensor, | ||
| 13 | + clamp_value: float, | ||
| 14 | + eps: float, | ||
| 15 | + save_for_backward: bool = False, | ||
| 16 | +) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: | ||
| 17 | + """Compute the Engram gate with eager PyTorch operations. | ||
| 18 | + | ||
| 19 | + This implementation is intentionally device-agnostic and supports | ||
| 20 | + autograd, making it the numerical oracle for both forward and backward | ||
| 21 | + TileLang kernels. | ||
| 22 | + """ | ||
| 23 | + | ||
| 24 | + num_tokens, hc_mult, hidden_size = hidden_states.shape | ||
| 25 | + scalar = hidden_size**-0.5 | ||
| 26 | + k, v = kv.split([hc_mult * hidden_size, hidden_size], dim=-1) | ||
| 27 | + k = k.view(num_tokens, hc_mult, hidden_size) | ||
| 28 | + | ||
| 29 | + x = hidden_states.float() | ||
| 30 | + k_float = k.float() | ||
| 31 | + weight_hidden_float = weight_hidden.float().unsqueeze(0) | ||
| 32 | + weight_embed_float = weight_embed.float().unsqueeze(0) | ||
| 33 | + | ||
| 34 | + rstd_x = torch.rsqrt(x.square().mean(-1) + eps) | ||
| 35 | + rstd_k = torch.rsqrt(k_float.square().mean(-1) + eps) | ||
| 36 | + raw_dot = torch.einsum( | ||
| 37 | + '...d,...d->...', | ||
| 38 | + x * weight_hidden_float, | ||
| 39 | + k_float * weight_embed_float, | ||
| 40 | + ) | ||
| 41 | + normalized_dot = raw_dot * rstd_x * rstd_k * scalar | ||
| 42 | + signed_sqrt = normalized_dot.abs().clamp_min(clamp_value).sqrt() * normalized_dot.sign() | ||
| 43 | + gate_score = signed_sqrt.sigmoid() | ||
| 44 | + output = (x + gate_score.unsqueeze(-1) * v.unsqueeze(-2)).bfloat16() | ||
| 45 | + | ||
| 46 | + if save_for_backward: | ||
| 47 | + return output, raw_dot, gate_score, rstd_x, rstd_k | ||
| 48 | + return output | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | +{ | ||
| 2 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=2048,]": { | ||
| 3 | + "peak_npu_mb": 32.1 | ||
| 4 | + }, | ||
| 5 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=2560,]": { | ||
| 6 | + "peak_npu_mb": 32.1 | ||
| 7 | + }, | ||
| 8 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=3072,]": { | ||
| 9 | + "peak_npu_mb": 32.1 | ||
| 10 | + }, | ||
| 11 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=4096,]": { | ||
| 12 | + "peak_npu_mb": 32.1 | ||
| 13 | + }, | ||
| 14 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=6144,]": { | ||
| 15 | + "peak_npu_mb": 32.2 | ||
| 16 | + }, | ||
| 17 | + "engram/test_engram_fused_weight.py::test_engram_fused_weight[hc=4,hidden=7168,]": { | ||
| 18 | + "peak_npu_mb": 32.2 | ||
| 19 | + } | ||
| 20 | +} | ||
| @@ -0,0 +1,38 @@ | |||
| 1 | +{ | ||
| 2 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=2048,]": { | ||
| 3 | + "peak_npu_mb": 198.2 | ||
| 4 | + }, | ||
| 5 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=2560,]": { | ||
| 6 | + "peak_npu_mb": 243.8 | ||
| 7 | + }, | ||
| 8 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=3072,]": { | ||
| 9 | + "peak_npu_mb": 290.3 | ||
| 10 | + }, | ||
| 11 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=4096,]": { | ||
| 12 | + "peak_npu_mb": 380.4 | ||
| 13 | + }, | ||
| 14 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=6144,]": { | ||
| 15 | + "peak_npu_mb": 562.6 | ||
| 16 | + }, | ||
| 17 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=512,hc=4,hidden=7168,]": { | ||
| 18 | + "peak_npu_mb": 653.7 | ||
| 19 | + }, | ||
| 20 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=2048,]": { | ||
| 21 | + "peak_npu_mb": 2861.9 | ||
| 22 | + }, | ||
| 23 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=2560,]": { | ||
| 24 | + "peak_npu_mb": 3572.0 | ||
| 25 | + }, | ||
| 26 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=3072,]": { | ||
| 27 | + "peak_npu_mb": 4288.3 | ||
| 28 | + }, | ||
| 29 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=4096,]": { | ||
| 30 | + "peak_npu_mb": 5705.2 | ||
| 31 | + }, | ||
| 32 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=6144,]": { | ||
| 33 | + "peak_npu_mb": 8550.4 | ||
| 34 | + }, | ||
| 35 | + "engram/test_engram_gate_bwd.py::test_engram_gate_bwd[num_tokens=8001,hc=4,hidden=7168,]": { | ||
| 36 | + "peak_npu_mb": 9976.5 | ||
| 37 | + } | ||
| 38 | +} | ||
| @@ -0,0 +1,38 @@ | |||
| 1 | +{ | ||
| 2 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=2048,]": { | ||
| 3 | + "peak_npu_mb": 102.1 | ||
| 4 | + }, | ||
| 5 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=2560,]": { | ||
| 6 | + "peak_npu_mb": 122.7 | ||
| 7 | + }, | ||
| 8 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=3072,]": { | ||
| 9 | + "peak_npu_mb": 143.2 | ||
| 10 | + }, | ||
| 11 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=4096,]": { | ||
| 12 | + "peak_npu_mb": 184.3 | ||
| 13 | + }, | ||
| 14 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=6144,]": { | ||
| 15 | + "peak_npu_mb": 266.4 | ||
| 16 | + }, | ||
| 17 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=512,hc=4,hidden=7168,]": { | ||
| 18 | + "peak_npu_mb": 307.5 | ||
| 19 | + }, | ||
| 20 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=2048,]": { | ||
| 21 | + "peak_npu_mb": 1302.9 | ||
| 22 | + }, | ||
| 23 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=2560,]": { | ||
| 24 | + "peak_npu_mb": 1622.3 | ||
| 25 | + }, | ||
| 26 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=3072,]": { | ||
| 27 | + "peak_npu_mb": 1943.1 | ||
| 28 | + }, | ||
| 29 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=4096,]": { | ||
| 30 | + "peak_npu_mb": 2583.4 | ||
| 31 | + }, | ||
| 32 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=6144,]": { | ||
| 33 | + "peak_npu_mb": 3865.9 | ||
| 34 | + }, | ||
| 35 | + "engram/test_engram_gate_fwd.py::test_engram_gate_fwd[num_tokens=8001,hc=4,hidden=7168,]": { | ||
| 36 | + "peak_npu_mb": 4506.2 | ||
| 37 | + } | ||
| 38 | +} | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | +{ | ||
| 2 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=2048,num_partials=30,]": { | ||
| 3 | + "peak_npu_mb": 33.0 | ||
| 4 | + }, | ||
| 5 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=2560,num_partials=30,]": { | ||
| 6 | + "peak_npu_mb": 33.3 | ||
| 7 | + }, | ||
| 8 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=3072,num_partials=30,]": { | ||
| 9 | + "peak_npu_mb": 33.5 | ||
| 10 | + }, | ||
| 11 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=4096,num_partials=30,]": { | ||
| 12 | + "peak_npu_mb": 34.1 | ||
| 13 | + }, | ||
| 14 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=6144,num_partials=30,]": { | ||
| 15 | + "peak_npu_mb": 35.1 | ||
| 16 | + }, | ||
| 17 | + "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=7168,num_partials=30,]": { | ||
| 18 | + "peak_npu_mb": 35.6 | ||
| 19 | + } | ||
| 20 | +} | ||
| @@ -0,0 +1,55 @@ | |||
| 1 | +import os | ||
| 2 | + | ||
| 3 | +import pytest | ||
| 4 | +import torch | ||
| 5 | + | ||
| 6 | +import cann_ops_tilelang | ||
| 7 | +from cann_ops_tilelang.config import get_device | ||
| 8 | +from tests.testing.bench import make_param_id | ||
| 9 | +from tests.testing.generator import generate_hidden_sizes, get_test_level | ||
| 10 | +from tests.testing.numeric import assert_equal, count_bytes | ||
| 11 | + | ||
| 12 | +os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0' | ||
| 13 | +pytestmark = pytest.mark.npu | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +def generate_test_data(params): | ||
| 17 | + hc_mult = params['hc'] | ||
| 18 | + hidden_size = params['hidden'] | ||
| 19 | + wh_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device()) | ||
| 20 | + we_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device()) | ||
| 21 | + return wh_data, we_data | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +def generate_test_params(level: int) -> list[dict]: | ||
| 25 | + del level | ||
| 26 | + return [{'hc': hc, 'hidden': hidden_size} for hc in (4,) for hidden_size in generate_hidden_sizes(256)] | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id) | ||
| 30 | +def test_engram_fused_weight(params): | ||
| 31 | + wh_data, we_data = generate_test_data(params) | ||
| 32 | + | ||
| 33 | + ref = wh_data.float() * we_data.float() | ||
| 34 | + out = cann_ops_tilelang.engram_fused_weight(wh_data, we_data) | ||
| 35 | + | ||
| 36 | + assert out.dtype == torch.float32 | ||
| 37 | + assert_equal(out, ref) | ||
| 38 | + | ||
| 39 | + | ||
| 40 | +@pytest.mark.benchmark | ||
| 41 | +@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id) | ||
| 42 | +def test_engram_fused_weight_benchmark(benchmark_timer, benchmark_record, params): | ||
| 43 | + wh_data, we_data = generate_test_data(params) | ||
| 44 | + out = cann_ops_tilelang.engram_fused_weight(wh_data, we_data) | ||
| 45 | + | ||
| 46 | + t_us = benchmark_timer(lambda: cann_ops_tilelang.engram_fused_weight(wh_data, we_data)) | ||
| 47 | + | ||
| 48 | + num_bytes = count_bytes(wh_data, we_data, out) | ||
| 49 | + benchmark_record( | ||
| 50 | + kernel='engram_fused_weight', | ||
| 51 | + operation='fwd', | ||
| 52 | + params=params, | ||
| 53 | + time_us=t_us, | ||
| 54 | + bandwidth_gbs=num_bytes / t_us / 1e3, | ||
| 55 | + ) | ||
| @@ -0,0 +1,120 @@ | |||
| 1 | +import os | ||
| 2 | + | ||
| 3 | +import pytest | ||
| 4 | +import torch | ||
| 5 | + | ||
| 6 | +import cann_ops_tilelang | ||
| 7 | +from cann_ops_tilelang.config import get_device, get_num_vector_cores | ||
| 8 | +from tests.testing.bench import make_param_id | ||
| 9 | +from tests.testing.generator import generate_hidden_sizes, get_test_level | ||
| 10 | +from tests.testing.numeric import calc_diff, count_bytes | ||
| 11 | + | ||
| 12 | +os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0' | ||
| 13 | +pytestmark = pytest.mark.npu | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +def engram_grad_w_reduce_ref( | ||
| 17 | + grad_w_partial, | ||
| 18 | + weight_hidden, | ||
| 19 | + weight_embed, | ||
| 20 | + grad_weight_hidden, | ||
| 21 | + grad_weight_embed, | ||
| 22 | +): | ||
| 23 | + grad_w_sum = grad_w_partial.sum(0) | ||
| 24 | + grad_weight_hidden.add_(grad_w_sum * weight_embed.float()) | ||
| 25 | + grad_weight_embed.add_(grad_w_sum * weight_hidden.float()) | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +def generate_num_partials(level: int) -> list[int]: | ||
| 29 | + gate_num_partials = (get_num_vector_cores() - 12) // 2 | ||
| 30 | + values = [gate_num_partials] | ||
| 31 | + if level >= 1: | ||
| 32 | + values.insert(0, max(gate_num_partials - 9, 1)) | ||
| 33 | + if level >= 2: | ||
| 34 | + values.insert(0, 1) | ||
| 35 | + return list(dict.fromkeys(values)) | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +def generate_test_params(level: int) -> list[dict]: | ||
| 39 | + return [ | ||
| 40 | + {'hidden': hidden_size, 'num_partials': num_partials} | ||
| 41 | + for hidden_size in generate_hidden_sizes(256) | ||
| 42 | + for num_partials in generate_num_partials(level) | ||
| 43 | + ] | ||
| 44 | + | ||
| 45 | + | ||
| 46 | +def generate_test_data(params): | ||
| 47 | + torch.manual_seed(0) | ||
| 48 | + hidden_size = params['hidden'] | ||
| 49 | + num_partials = params['num_partials'] | ||
| 50 | + hc_mult = 4 | ||
| 51 | + device = get_device() | ||
| 52 | + grad_w_partial = torch.randn(num_partials, hc_mult, hidden_size, dtype=torch.float32, device=device) | ||
| 53 | + weight_hidden = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device) | ||
| 54 | + weight_embed = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device) | ||
| 55 | + return grad_w_partial, weight_hidden, weight_embed | ||
| 56 | + | ||
| 57 | + | ||
| 58 | +@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id) | ||
| 59 | +def test_engram_grad_w_reduce(params): | ||
| 60 | + grad_w_partial, weight_hidden, weight_embed = generate_test_data(params) | ||
| 61 | + output_shape = weight_hidden.shape | ||
| 62 | + device = weight_hidden.device | ||
| 63 | + grad_weight_hidden_ref = torch.randn(output_shape, dtype=torch.float32, device=device) | ||
| 64 | + grad_weight_embed_ref = torch.randn(output_shape, dtype=torch.float32, device=device) | ||
| 65 | + grad_weight_hidden = grad_weight_hidden_ref.clone() | ||
| 66 | + grad_weight_embed = grad_weight_embed_ref.clone() | ||
| 67 | + | ||
| 68 | + engram_grad_w_reduce_ref( | ||
| 69 | + grad_w_partial, | ||
| 70 | + weight_hidden, | ||
| 71 | + weight_embed, | ||
| 72 | + grad_weight_hidden_ref, | ||
| 73 | + grad_weight_embed_ref, | ||
| 74 | + ) | ||
| 75 | + cann_ops_tilelang.engram_grad_w_reduce( | ||
| 76 | + grad_w_partial, | ||
| 77 | + weight_hidden, | ||
| 78 | + weight_embed, | ||
| 79 | + grad_weight_hidden, | ||
| 80 | + grad_weight_embed, | ||
| 81 | + ) | ||
| 82 | + for name, actual, expected in ( | ||
| 83 | + ('grad_weight_hidden', grad_weight_hidden, grad_weight_hidden_ref), | ||
| 84 | + ('grad_weight_embed', grad_weight_embed, grad_weight_embed_ref), | ||
| 85 | + ): | ||
| 86 | + diff = calc_diff(actual, expected) | ||
| 87 | + assert diff < 1e-10, f'{name} mismatch: {diff:.6e}' | ||
| 88 | + | ||
| 89 | + | ||
| 90 | +@pytest.mark.benchmark | ||
| 91 | +@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id) | ||
| 92 | +def test_engram_grad_w_reduce_benchmark(benchmark_timer, benchmark_record, params): | ||
| 93 | + grad_w_partial, weight_hidden, weight_embed = generate_test_data(params) | ||
| 94 | + output_shape = weight_hidden.shape | ||
| 95 | + device = weight_hidden.device | ||
| 96 | + grad_weight_hidden = torch.randn(output_shape, dtype=torch.float32, device=device) | ||
| 97 | + grad_weight_embed = torch.randn(output_shape, dtype=torch.float32, device=device) | ||
| 98 | + time_us = benchmark_timer( | ||
| 99 | + lambda: cann_ops_tilelang.engram_grad_w_reduce( | ||
| 100 | + grad_w_partial, | ||
| 101 | + weight_hidden, | ||
| 102 | + weight_embed, | ||
| 103 | + grad_weight_hidden, | ||
| 104 | + grad_weight_embed, | ||
| 105 | + ) | ||
| 106 | + ) | ||
| 107 | + num_bytes = count_bytes( | ||
| 108 | + grad_w_partial, | ||
| 109 | + weight_hidden, | ||
| 110 | + weight_embed, | ||
| 111 | + grad_weight_hidden, | ||
| 112 | + grad_weight_embed, | ||
| 113 | + ) | ||
| 114 | + benchmark_record( | ||
| 115 | + kernel='engram_grad_w_reduce', | ||
| 116 | + operation='fwd', | ||
| 117 | + params=params, | ||
| 118 | + time_us=time_us, | ||
| 119 | + bandwidth_gbs=num_bytes / time_us / 1e3, | ||
| 120 | + ) | ||
| @@ -0,0 +1,138 @@ | |||
| 1 | +"""Improvement summary pytest plugin for ops-tilelang. | ||
| 2 | + | ||
| 3 | +Computes per-kernel geometric-mean latency, bandwidth, and speedup | ||
| 4 | +from benchmark results collected during the session, and prints | ||
| 5 | +a summary table comparing ops-tilelang against legacy kernels. | ||
| 6 | + | ||
| 7 | +This file is deliberately NOT named ``conftest.py`` -- it is loaded via | ||
| 8 | +``pytest_plugins`` in the root ``conftest.py``. A non-conftest name | ||
| 9 | +prevents pluggy's duplicate-registration error. | ||
| 10 | +""" | ||
| 11 | + | ||
| 12 | +import json | ||
| 13 | +import os | ||
| 14 | +import statistics | ||
| 15 | + | ||
| 16 | +import pytest | ||
| 17 | + | ||
| 18 | +from tests.testing.bench import make_param_id | ||
| 19 | +from tests.testing.stash import BENCHMARK_RESULTS | ||
| 20 | + | ||
| 21 | +_IMPROVEMENT_SUMMARY_KEY = pytest.StashKey() | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +def pytest_addoption(parser): | ||
| 25 | + parser.addoption( | ||
| 26 | + '--legacy-table', | ||
| 27 | + action='store_true', | ||
| 28 | + default=False, | ||
| 29 | + help='Print a performance summary table comparing ops-tilelang vs legacy kernels', | ||
| 30 | + ) | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +def _build_key(rec): | ||
| 34 | + kernel, operation = rec['kernel'], rec['operation'] | ||
| 35 | + prefix = f'{rec["cluster"]}/{kernel}/{operation}' | ||
| 36 | + params = rec.get('params') | ||
| 37 | + if params: | ||
| 38 | + return f'{prefix}[{make_param_id(params)}]' | ||
| 39 | + return prefix | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +def _compute_improvement_summary(config): | ||
| 43 | + if not config.getoption('--legacy-table', default=False): | ||
| 44 | + return None | ||
| 45 | + | ||
| 46 | + results = config.stash.get(BENCHMARK_RESULTS, []) | ||
| 47 | + if not results: | ||
| 48 | + output_path = config.getoption('--benchmark-output', default=None) | ||
| 49 | + if output_path and os.path.exists(output_path): | ||
| 50 | + with open(output_path) as f: | ||
| 51 | + results = [json.loads(line) for line in f if line.strip()] | ||
| 52 | + if not results: | ||
| 53 | + return None | ||
| 54 | + | ||
| 55 | + deduped = {} | ||
| 56 | + for rec in results: | ||
| 57 | + key = _build_key(rec) | ||
| 58 | + if key in deduped: | ||
| 59 | + existing = deduped[key] | ||
| 60 | + if existing.get('extras', {}).get('speedup') is not None: | ||
| 61 | + continue | ||
| 62 | + deduped[key] = rec | ||
| 63 | + | ||
| 64 | + kernel_records = {} | ||
| 65 | + for rec in deduped.values(): | ||
| 66 | + kernel = rec['kernel'] | ||
| 67 | + kernel_records.setdefault(kernel, []).append(rec) | ||
| 68 | + | ||
| 69 | + summary = [] | ||
| 70 | + for kernel in sorted(kernel_records.keys()): | ||
| 71 | + records = kernel_records[kernel] | ||
| 72 | + latencies = [r['time_us'] for r in records] | ||
| 73 | + bws = [r.get('bandwidth_gbs') for r in records if r.get('bandwidth_gbs') is not None] | ||
| 74 | + speedups = [ | ||
| 75 | + r.get('extras', {}).get('speedup') for r in records if r.get('extras', {}).get('speedup') is not None | ||
| 76 | + ] | ||
| 77 | + | ||
| 78 | + if not latencies: | ||
| 79 | + continue | ||
| 80 | + | ||
| 81 | + avg_latency = statistics.geometric_mean(latencies) | ||
| 82 | + avg_bw = statistics.geometric_mean(bws) if bws else None | ||
| 83 | + avg_speedup = statistics.geometric_mean(speedups) if speedups else None | ||
| 84 | + | ||
| 85 | + summary.append( | ||
| 86 | + { | ||
| 87 | + 'kernel': kernel, | ||
| 88 | + 'latency_us': avg_latency, | ||
| 89 | + 'bw_gbs': avg_bw, | ||
| 90 | + 'speedup': avg_speedup, | ||
| 91 | + } | ||
| 92 | + ) | ||
| 93 | + | ||
| 94 | + return summary | ||
| 95 | + | ||
| 96 | + | ||
| 97 | +def pytest_sessionfinish(session, exitstatus): | ||
| 98 | + summary = _compute_improvement_summary(session.config) | ||
| 99 | + session.config.stash[_IMPROVEMENT_SUMMARY_KEY] = summary | ||
| 100 | + | ||
| 101 | + | ||
| 102 | +def pytest_terminal_summary(terminalreporter, config): | ||
| 103 | + summary = config.stash.get(_IMPROVEMENT_SUMMARY_KEY, None) | ||
| 104 | + if summary is None: | ||
| 105 | + return | ||
| 106 | + | ||
| 107 | + tr = terminalreporter | ||
| 108 | + | ||
| 109 | + kw = max(max(len(s['kernel']) for s in summary), 20) + 2 | ||
| 110 | + lat_w = 12 | ||
| 111 | + bw_w = 10 | ||
| 112 | + sp_w = 10 | ||
| 113 | + total_w = kw + lat_w + bw_w + sp_w | ||
| 114 | + | ||
| 115 | + tr.write_line('=' * total_w) | ||
| 116 | + tr.write_line(' ops-tilelang vs legacy -- Performance Summary') | ||
| 117 | + tr.write_line('=' * total_w) | ||
| 118 | + hdr = f'{"Kernel":<{kw}} {"Latency(us)":>{lat_w}} {"BW(GB/s)":>{bw_w}} {"Speedup":>{sp_w}}' | ||
| 119 | + tr.write_line(hdr) | ||
| 120 | + tr.write_line('-' * total_w) | ||
| 121 | + | ||
| 122 | + speedups_for_overall = [] | ||
| 123 | + for s in summary: | ||
| 124 | + latency_str = f'{s["latency_us"]:.1f}' | ||
| 125 | + bw_str = f'{s["bw_gbs"]:.0f}' if s['bw_gbs'] is not None else '-' | ||
| 126 | + if s['speedup'] is not None: | ||
| 127 | + speedup_str = f'{s["speedup"]:.2f}x' | ||
| 128 | + speedups_for_overall.append(s['speedup']) | ||
| 129 | + else: | ||
| 130 | + speedup_str = 'N/A' | ||
| 131 | + line = f'{s["kernel"]:<{kw}} {latency_str:>{lat_w}} {bw_str:>{bw_w}} {speedup_str:>{sp_w}}' | ||
| 132 | + tr.write_line(line) | ||
| 133 | + | ||
| 134 | + tr.write_line('-' * total_w) | ||
| 135 | + if speedups_for_overall: | ||
| 136 | + overall = statistics.geometric_mean(speedups_for_overall) | ||
| 137 | + tr.write_line(f' Geometric-mean speedup ({len(speedups_for_overall)} kernels): {overall:.2f}x') | ||
| 138 | + tr.write_line('') | ||
čæęÆnnä»ēé ē½®ļ¼ https://gitcode.com/cann/ops-nn/blob/master/.pre-commit-config.yaml
Python 代ē ę£ę„