From f026dd0454de97f8332b5d46c4adb63cf4f640d2 Mon Sep 17 00:00:00 2001 From: Dineth Hettiarachchi Date: Mon, 26 Oct 2026 11:58:01 +0530 Subject: [PATCH 1/2] feat: Phase 9 docs, landing page and launch assets - Landing page with the animated FLASH schedule (N = 16, causal), the three claims, and links to the visualizer and docs. - Six docs pages with live demos: what attention computes (float), the online softmax (engine run recorded tile by tile), the FlashAttention traffic (generated table), fixed point (INT8 quantizer, measured error), the architecture diagram, and verification (each check with its command, and the measured tables). Plus a docs index. - Generated web/src/lib/measured.ts from the golden model, the recorded 1000-seed study and the N = 32 cycle baselines; pytest fails if it or the other generated tables are stale. - Social card at /og (1200 x 630) captured to web/public/og.png; screenshot to docs/media/visualizer.png; og and twitter meta tags in the layout. - README rewritten for the series: live link, verification table with the measured numbers, repository map, quick start. - Browser tests for every page (status, no console errors, islands render, demos respond) and every page at 375 px. - --line colour token added to both themes. Docs tables scroll inside their own box on phones. --- CONTEXT.md | 6 +- README.md | 73 +++++++-- docs/ARCHITECTURE.md | 22 +++ docs/PLAN.md | 2 +- docs/media/visualizer.png | Bin 0 -> 65226 bytes sim/golden/export_web_data.py | 66 +++++++- sim/golden/test_golden.py | 1 + web/e2e/docs.spec.ts | 97 ++++++++++++ web/public/og.png | Bin 0 -> 49286 bytes web/scripts/capture-assets.mjs | 55 +++++++ web/src/components/docs/AttentionDemo.tsx | 50 ++++++ web/src/components/docs/DocsLayout.astro | 51 ++++++ web/src/components/docs/Hero.tsx | 58 +++++++ web/src/components/docs/OnlineSoftmaxDemo.tsx | 40 +++++ web/src/components/docs/QuantizerDemo.tsx | 63 ++++++++ web/src/components/docs/TrafficDemo.tsx | 63 ++++++++ web/src/layouts/Layout.astro | 13 ++ web/src/lib/measured.ts | 147 ++++++++++++++++++ web/src/lib/record.ts | 47 ++++++ web/src/pages/docs/architecture.astro | 90 +++++++++++ web/src/pages/docs/fixed-point.astro | 79 ++++++++++ web/src/pages/docs/flashattention.astro | 36 +++++ web/src/pages/docs/index.astro | 15 ++ web/src/pages/docs/online-softmax.astro | 32 ++++ web/src/pages/docs/verification.astro | 105 +++++++++++++ web/src/pages/docs/what-is-attention.astro | 33 ++++ web/src/pages/index.astro | 56 +++++-- web/src/pages/og.astro | 53 +++++++ web/src/styles/global.css | 2 + 29 files changed, 1333 insertions(+), 22 deletions(-) create mode 100644 docs/media/visualizer.png create mode 100644 web/e2e/docs.spec.ts create mode 100644 web/public/og.png create mode 100644 web/scripts/capture-assets.mjs create mode 100644 web/src/components/docs/AttentionDemo.tsx create mode 100644 web/src/components/docs/DocsLayout.astro create mode 100644 web/src/components/docs/Hero.tsx create mode 100644 web/src/components/docs/OnlineSoftmaxDemo.tsx create mode 100644 web/src/components/docs/QuantizerDemo.tsx create mode 100644 web/src/components/docs/TrafficDemo.tsx create mode 100644 web/src/lib/measured.ts create mode 100644 web/src/lib/record.ts create mode 100644 web/src/pages/docs/architecture.astro create mode 100644 web/src/pages/docs/fixed-point.astro create mode 100644 web/src/pages/docs/flashattention.astro create mode 100644 web/src/pages/docs/index.astro create mode 100644 web/src/pages/docs/online-softmax.astro create mode 100644 web/src/pages/docs/verification.astro create mode 100644 web/src/pages/docs/what-is-attention.astro create mode 100644 web/src/pages/og.astro diff --git a/CONTEXT.md b/CONTEXT.md index e8d31ad..86dcc10 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -38,7 +38,11 @@ On a fresh machine delete the symlinks and run `.tools/setup.sh` and `.tools/ems FLASH), Playwright smoke test (5 tests, production build). - Phase 8 (L1 softmax cell, L2 tile dataflow, L4 naive vs flash, N sweep, error panel): DONE. Engine readouts `tileDone` and `getDatapath`. 12 Playwright tests, 15 Vitest, Node WASM test, one-seed cocotb. Light-mode screenshots not reviewed. -- Next: Phase 9 (docs, landing page, launch). Also open: the `astro dev` hang. +- Phase 9 (docs, landing page, launch): built. Landing page with the animated FLASH schedule, six docs pages with live + demos, `/og` social card, generated `measured.ts`, README rewritten for the series. Deployment: see the PR and + ARCHITECTURE §14. +- Next: Phase 10 (FPGA synthesis, ECP5 for the ULX3S). Blocked here: no yosys / nextpnr / ecppack installed, and the disk + had under 1 GB free during Phase 9. ## Gotchas found in Phase 8 - The debug datapath wires are idle between tiles. The L2 view keeps the last busy clock's values, or the panel shows zeros. diff --git a/README.md b/README.md index 1325cd0..426510f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,47 @@ # TinyAttention -A real, synthesizable **FlashAttention engine** in SystemVerilog, compiled to WebAssembly and running live -in your browser: online softmax with a running max and sum, SRAM tiles filling and emptying, and standard -vs FlashAttention memory traffic side by side. +A synthesizable **FlashAttention engine** in SystemVerilog, verified bit-exact against a golden model, compiled to +WebAssembly, and running live in your browser. + +**Live:** https://tiny-attention.vercel.app + +![The visualizer running one FLASH pass](docs/media/visualizer.png) Fourth project in the series after [tiny-gpu](https://github.com/deaneeth/tiny-gpu), -[tiny-tpu](https://github.com/deaneeth/tiny-tpu) and tiny-cpu. Same rule as the others: nothing on screen is -faked; every value comes from a `dbg_*` port on the compiled RTL. +[tiny-tpu](https://github.com/deaneeth/tiny-tpu) and tiny-cpu. Same rule as the others: nothing on screen is faked. +Every value comes from a debug port on the compiled RTL, or from the golden model the RTL is checked against. + +## What it does + +Attention computes `softmax(Q Kᵀ / √d) V`. The score matrix is N × N, and storing it is what makes standard attention +expensive in memory. FlashAttention computes the output one tile at a time with an online softmax, so the matrix never +exists in memory. This engine implements that schedule, plus the standard three-pass schedule for comparison, with +optional overlap of the K/V loads with compute. + +## Verification -**Status:** Phases 0-8 done: the engine (FLASH, NAIVE, K/V overlap) is bit-exact against the golden model and compiled to WebAssembly. The browser visualizer at `/app` has four views (softmax cell, tile dataflow, schedule, naive vs flash with an N sweep) with editable inputs, K/V buffers, HBM traffic against the model, and 12 Playwright tests. Next: the landing page and the documentation pages. See [`docs/PLAN.md`](docs/PLAN.md) for the roadmap. +| What | Result | +|---|---| +| Engine vs golden model, end to end | Output, HBM bytes, tile count, K/V loads and busy cycles match on every run. Every sequence length 4 to 32, causal and not, every block size, both schedules, overlap on and off, random HBM latency, 200 seeds. | +| Exhaustive unit coverage | exp2 is checked on every input in its range; reciprocal on every mantissa. DMA burst timing is pinned cycle by cycle. | +| Output error vs float attention | Max 0.0028 to 0.0260 across the six presets (N = 12 to 16), in float units. The recorded 1000-seed study is in `sim/golden/study_results_1000seeds.txt`. | +| HBM traffic at N = 32 | Non-causal: FLASH 2432 B (4-row blocks) or 896 B (16-row blocks), NAIVE 12672 B. Causal: FLASH 1536 B or 768 B, NAIVE 7296 B. | +| WebAssembly | The compiled module, on golden fixtures for each schedule, matches the same quantities. 83 KB gzipped. | +| Web app | Browser tests of every page, the visualizer at phone width and in both colour schemes. | +| FPGA | Not yet synthesized (Phase 10). No timing or area numbers are claimed. | + +Every row is a test in the repository, run by GitHub Actions on each pull request. The docs page +[Verification](https://tiny-attention.vercel.app/docs/verification) lists each check and its numbers. + +## Repository + +``` +rtl/ SystemVerilog: arithmetic blocks, tile core, memory, controllers, top level +sim/ cocotb unit and end-to-end tests, the golden model (sim/golden), lint and run scripts +wasm/ Verilator + Emscripten build, the C++ harness, and the Node test over golden fixtures +web/ Astro + React site: the visualizer at /app, the docs, and the Vitest and Playwright tests +docs/ Architecture, the plan, the state schema, and the media for this README +``` ## Quick start @@ -17,10 +50,32 @@ bash .tools/setup.sh && bash .tools/emsdk.sh # Verilator 5.036 + Emscripten python3 -m venv .venv && .venv/bin/pip install -r sim/requirements.txt source scripts/env.sh -bash wasm/build.sh && node wasm/test.mjs # engine WASM + golden fixtures -bash wasm/spike/build.sh && node wasm/spike/test.mjs # toolchain spike -cd web && pnpm install && pnpm dev # open /spike +bash sim/lint.sh # Verilator -Wall on every module +bash sim/run_unit.sh # cocotb unit suites +SEEDS=200 bash sim/run_e2e.sh # engine vs golden, end to end +pytest sim/golden -q # golden model, fixtures, generated tables + +bash wasm/build.sh && node wasm/test.mjs # WebAssembly engine + golden fixtures + +cd web && pnpm install && pnpm dev # the site (dev server; see note below) +pnpm build && pnpm test:e2e # production build and browser tests ``` +Note: `astro dev` hangs without output in some environments. `pnpm build` followed by any static file server over +`web/dist` works. + +## Docs + +Six pages with live demos: [what attention computes](https://tiny-attention.vercel.app/docs/what-is-attention), +[the online softmax](https://tiny-attention.vercel.app/docs/online-softmax), +[the FlashAttention schedule](https://tiny-attention.vercel.app/docs/flashattention), +[fixed-point formats and error](https://tiny-attention.vercel.app/docs/fixed-point), +[the architecture](https://tiny-attention.vercel.app/docs/architecture), and +[verification](https://tiny-attention.vercel.app/docs/verification). + +The plan and the status of each phase are in [`docs/PLAN.md`](docs/PLAN.md). The architecture, with the measured +schedule and traffic models, is in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). + ## License + MIT diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d7f3655..8b90b88 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -421,3 +421,25 @@ Gaps against the plan's Phase 8 text: Checks: `pnpm test:e2e` (12 tests, including all four views at 375 px and the light/dark background), `pnpm test` (15), `node wasm/test.mjs`, `SEEDS=1 bash sim/run_e2e.sh`, `bash sim/lint.sh`, `pnpm lint`, `pnpm typecheck`, `pnpm build`. + +## 14. Phase 9: docs, landing page, launch + +- **Landing page** (`/`): the hero animates the FLASH schedule for N = 16 on a causal mask (pure React, no engine), with + the three claims the site makes and links to the visualizer and the docs. Respects `prefers-reduced-motion`. +- **Docs** (`/docs/*`, six pages, plus an index). Each page has a live demo of what it explains: + - *What is attention*: float attention for a chosen preset row (the reference, not the engine). + - *Online softmax*: a FLASH run of the toy sentence recorded tile by tile (`web/src/lib/record.ts`), shown by the L1 view. + - *FlashAttention*: HBM bytes and tile counts for FLASH and NAIVE from the generated traffic table. + - *Fixed point*: an INT8 quantizer for one Q row and one K row, with the folded cfg_scale in hex; the formats table; + measured error from the generated `measured.ts`. + - *Architecture*: a static block diagram of the RTL. + - *Verification*: each check and its command, with the measured tables. +- **Generated numbers.** `sim/golden/export_web_data.py` writes `web/src/lib/measured.ts`: the per-preset error (golden + model), the recorded 1000-seed study (rows parsed from `study_results_1000seeds.txt`), and the N = 32 cycle baselines + (from `sim/perf_baseline.json`). `pytest sim/golden` fails if any generated file is stale. +- **Social card and screenshots.** `/og` is a 1200 × 630 card; `web/scripts/capture-assets.mjs` captures it to + `web/public/og.png` and the visualizer to `docs/media/visualizer.png`. The card is in the page's `og:image` meta tags. +- **Tests.** Playwright covers every page (no console errors, status 200, the islands render, the quantizer and traffic + demos respond) and every page at 375 px without horizontal scroll. Tables scroll inside their own box on phones. +- **Not in this phase:** FPGA numbers (Phase 10). The README and the Verification page say so. + diff --git a/docs/PLAN.md b/docs/PLAN.md index 19bec65..411af74 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -456,7 +456,7 @@ The estimates assume one person part-time, at tiny-cpu's pace. - L1 Softmax cell, L2 Tile dataflow, L4 Naive vs Flash (two instances, N sweep, error panel). - **Exit:** every level works on desktop and at 375 px, in light and dark themes. -### Phase 9: Docs, landing page, launch (1 week) +### Phase 9: Docs, landing page, launch (1 week) ✅ DONE (see ARCHITECTURE §14; deployed as noted there) - The six docs pages, each with an embedded live mini-visualizer. Hero capture and OG image (reuse the tiny-tpu scripts). - README in the series style: live demo, verification table (bit-exact seeds, exhaustive unit coverage, measured error, HBM savings at N = 32), and FPGA numbers. diff --git a/docs/media/visualizer.png b/docs/media/visualizer.png new file mode 100644 index 0000000000000000000000000000000000000000..9500c980252fd472c7a2c6509b135f2c00944b67 GIT binary patch literal 65226 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

C0~v0WG}?q2HtHV2KN7Ex)@ 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

CvE-U!rF;ihcy>TE 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

Rivl1#2-LMSn2wf0yY0U)I#AigufS zRiT&V6Rb^Xch%j4*&@`Mb;ShixeR+pHJprYy0F>5)S%!Jnet^68|gD@Zafh{HJ@@2Ob)wPPegp zNt$=mA@2wvemZx`E6V?v2vcnvPUzk2}(tppkm^D8$ge{iZ37NA>kM0g+BYTX$^ru_|^G; zQ7KNyVTi(?<$k6SuP%S*>-}n8YWrulfN!s2_@BP)q7dbO^LtqG@F|fz4eQu`D{UjV zW)5Iw0d53?_ea*kvwRj^jj><_JECglM!oV6co9UfNG6@9p`@l0?;jqH*>{1V{ARNP zT+s_z2 zlvon463W4=?ZA+DhjH(d@$6foVRRggQEi*xWVHG#4U{?)1qxoDZeFJ&eOq4hNbJxyiNk?jhtmW%N85o zT)j*w5%%U=$D_bgybWVVU`qpc#-=*)PH9RbZH4_nQL;qyK|v{Wz_HuQuw$6s_n%W<#r)dP7rUC zwO*fj-a}`k0-8k_P4=Qhluf7ZAWI&O@>^m)f)pigZl)s&_jkP>We!_ii+7DD`73VD zfZsAFL~MH7$3eubpu37|nBIAK5ygBR32!mE)BcWBz}jSx{Anneo$o-=lncN^k>94o z>bs1Dr|%HXlrqhLC3iu=-fH}Ph37Y!#R&P+H6hp;3{J97M2dEYP~CM^%McuU;o24MlkYl#~A-q_7lzQe@xvNO! zHow-<8}%H1daMHY@j zIho2)cN5YSm(-DwU{i3EMJmX0UfG=PUQt`UNR=1C02i2kS#yE zucYQmQ>`&k@{rc@uo2pg1txu4Rk=9LEhGgoRYjb}o2jxsU_bv)brd}#5dccmRxcFjUE zTV8zUEdKK=bvCFa(6U=kxaXJNMn9oa{l1SR5LL82Z;3;x*V9wa(WNS*KM4R*blELN zEF#~8O`*EG{UUf`5nzilB^eSteC0lICm=&*ub(e_dSrC75Wi=y9G=C$?>j{6C7L>g-sM=%J zh-JIrI!bl|*o3h=-i4zHMq@gw1Fz_5bp(2Ue@-@~K9j+Tu8*-Gfhya| z8oTtHJq>)OZ73*Cl9eL_g|H$(Ql;s3a7GY@R~RhOTe#z!iz{I=QW1Y4DloqxX*(&BcoFf>1PV=A(E)ChnX>Qw730dLl~ zt|EBu_bO??bn9ZIn+&PAbUi2*a8(Golj^hIk9E4;Jc(ey-=@6YksbS;69KsYT})gm z;6O!le*z#(A)@z$DMeoI7@!)4Myc(fkB??-ZCPvXLSBIBdAP{MP3QQm4DL8$4(vxM zw!@**hR|DS07!0du{Xo!gaVa&7u|&6agfE&Q150h7ufhuVt|*a-Yn}n#5N-yAmmLs zu)d3qr%mP+rXZRDPJW1M*vexLjT)v~OxiEemwV4gOv*Svv^|d9Wi;QEy)KCK#7lyR z)E5>p7T2ibei(K#W_52Me$^@ng7ow;7`tx;vXGF80c)-xQ-+N#qk{ts4cmj91*A+< z`-`S_C~V@foXG%cMd|hqT-cV1l)*-g%!2kg9La5drCtN2wfJzZWa8mrr?@RbWUds~ zPlpk=#708?l}A}Q?V@Kcph%5+)+iDRaj~}_h8~;dX0t8wZ3!#apPfW6;ot7m(Wsq0A4vWrNxv-<%jQ{?L zVQwzBTamb;K<`;E+kFnWEZAt?W7DC;N}#b@^M`5zOh|ODnaWwUxSjFiwluhRjFFkk zPSb-wk(ZH2Jg;)x4+*sj3+uOh41aI2BTD$2cfEXcS4X8S<2l97`aQl|hqB$vtV4IC zdAX~%xs?NDO&rGSy)Pr~Nt+LM%lK~W=b$N*-OXEFgQ@tg&I49y(<<2=zXuvG7Mks2 zq~BOU6KmkCa<1?Vgue|>pz_`SQ33{(@ybSX*j1hm^g-M#tIX=G=b#1<*uqb;phj)# z#(8DC{nwTm&qs|jZV`fpJ<&RA!ZmmmKoj`t|G=x6eybo|})?t%MPsu?yL*&7F z4J|T~;=AmcT(#JoX?EjCNm^1lA0Sp=$!C1d+!rH*Z8Cjn+*;XLOk0c(+H@6h)^^I^ zuOym2jk~Go;&o{!=_ssPO8faCHi?h9hIM;3*sfA)V$-~)%dF=(5|!OjIf>Xf3^9%& z*wOPdlPJQIx>LAAmSvc{9nJoh30bad*~UgJB!0-$eJzMt-UW4;8RR3(26Yf?FtTmoe$w)Qw7sxd0wrIhZmt)L1DqEZ1&xG#Ix~KM%;Y&y)Miza+$bEED`9Rv@OEsdn@&`A z;WUilPxhCSdpEjxY`q@iY!#joL-?K~F}3%2JuyeKCA#>cCjfAh?jXddM=sgyI4LuB z!F#r-TKkGEcHaXQSvDo2?clKDTeMl`dPmrud65o-nQ=;uycfP!GVi6bFxA}ao4CKq z*^P|~PhJcD864Lq9;?$G9bTa(HY^Sd8^21$=e~zxUmyWe)rS2d-Xfz5BcqGU#Ak(m z*--44{?PChZ@Iod9IHhOO>K2tQE(m0s)3Lj-)%5wKPQpM2=_oTHL}`j|IU$l(B?NM z4Jx!m5}SQ8gNipdD4dxi!b2@%d^*<2x7Oih{b{*SO7j+<$$eVW?awTckfKJILs+~a zh*s6son@Amjws4|Avd&3q!EDq#`i%}d@o(|-2|hoNyK_4# zt6aLhItS&tpY>ftu)AoOUtq2e-I$eXTqkP}s3qOav{-nUf@QT24wIUs2aKDShKImz zb2F>~klTE7h>~nB_iW6n_4qiqZ|RZRh93oTIuEZh?Sdg{aPS-SY-_PB8-LW0sOEs8 zu1ba)!w3~F6KMgL#qA(Xj=zLThv}=&bOCdyt>*pO-wA5>dOb{$+ z7!KJvPq%$5M)-F~cIOu|}owVfx_q zy^U#YtFw8^U%8!M2?7$ zi^;e9JvK}WmvM6gPhSX0ORsIGeHLYxN=M$jyEE^llrf8xqgw@-081s~C_19DW z=bQ69=cm>D+Y;^G_eU$_RkhC-JS&qH6zD%90;oJ=ukJUVbELk;*+!t!45r}^Zd#UVG}p&ZJX!o z<%)Z&6!2RKK5RwH&ycf;-$MHBGTgQuc{Z5Hyp64 zmy#DRVrrFL0ETN(?Mlx>_1gACf@AD9TqXFGWcOnEk^S;Y8TKVi!St@T6G5x*)$c-a zeqtoy3plUH5M8drwn@RIuN|}+_s-26DC}CGhk=8+Nk5}!i{gr#JfYaB>3^_TY}_?rP=>a#_eW01zCw@`Nd?-Z7YI zHJN1B+zlHghuClT&97e~i>o%+H{B`H#!8?95k?e@sA6f}z?Z$_)&&v zrE0@$s`h~3Jj?u!iTS9;58|x51N6KZqflz&oJg*9v(fKtT9=WuXDz3!-OFBpSu{8* zc7=|#eRpc%Am@I0Vx%51?>;sk+QzPve>JXtG)G3Kn7+NBrO)K=sMuMPuFFpZZzHEvO00kJxX~RGuC%rqrI_)T2uNJT<$nLZ z9%^~vo8(T^e7aKfTB9%40<<2xwZwDWKPyy{^CJ#SW&wUt3z}Lk#~3 zhny-aGO%D+c&xAIFBO|o=%G~Wfv#43(q9G-&{2k2InP~ z9>47i7jI{3V5F9WWt?Y2^aQrx&YRwpqpY=lpSZP+Gf(4M3t65Ea6hqt6{);CW@fw&Jh#12FDs8I(q;jCrB==B~C3e2s5gjmA`E;XM z3m(BE5!EZBrYSq%FMukWq{L!M+-Q%Zv9|h`^BG%sPJs8%P~ResuqdjU?+@8D)*&`P z@~(FhOpMOc>E?)n-wmca$|~ktSDxy-Z(?a)Z~K4UN}>*yo^(nfWTjS9*LJPTH-p@7 zYH8&;j(;Gaw!B}%vbfBrEYWbQE1q*11SO`5-Uu#>0GJVABQ9`%u$#h5{4FACWXv}c zs;aZCWZbd=zJn-uWX&E8;pu6(7NT!UA<=6Acyg*|g%Eyt^5jtN#eeY_|8h&h}umG~Hc_`3c^i&|jW8*CpZ@1onZJ>o% zaYz7=f6C46J&4rYRZTp*LccwicIL;?@J>43fM>(z_p=<7%&99O>Dx{_0DDpzWOG>x zE@`^k)K~QeK4OZ>^YH1$ra|5kJyZ8ACy$*K7VMdRonR+{_E z@%c=Yu~`wKK?_Y`Cavg5-Ixm)8fZx2qN}IT12j}tJ5@cX(5=sN-PX(Da-(;_V^F}U zS*%6`aN1_QCQUD%lcz*f6BIAQPiG|)!S~N$j_beqv^yakjt?J7r$#T@(rgmgo6D(z z#|nes=y<;Jx^n2=Ke2$1gOQErIcFe)fMI>s_{4RQgITH`!}YZ}nL{0aU!0y+Z-XgF ze}^C8kClTExx%i>J);gL7Z8CZCbnwW8asTr~j&usgLLD0*UK0sEzvSSKoosYx z%qAb=lo)etB*si7I|!)U$B~duyk8S5fgA@OYOY(Dwc({Q)b(fL(;G)rJ{d%vvgoOD z#E&1rrO*Pf^YV>mIH(}nv0F`77b!yPO35S=Tbd91#p*sJ@i<8fH^^}gELENl(B)na z7AfGZFmDVbV$<~Gmdq7RSL{Gv4$Z)J-Px<2+|zY-Na( zJ0vyIx6x0BS8Eu{lByREw$dq{O;NYq`4|)yD}LN0WB~racrl_)7o^Y!0s_vCH!-^t z<#iR1c8>dt(UUtH+Q+fee|#pJJgo0@puH9%*U&q~qMdU}n6(f809qxD(6Un03Txc0 zhme(cZa^wY8bNNZGhfVf;$|3a=C{^FAsX-FU%<P_l*3MP7bQ?*hN_h|;k4(g<&? zD+PbV*nv>#fD(dwx&gZ-yfbK13+6o)u9@`QQv|N_WZk&&VR4@V%wsi$94YljL+hD= zfWzTeRqdv7BDmcJ6u3)+1F^be3h(_T!V{1D>HILsy2_8{xW;SZ$?Czs+qCRqeg4JbLhfQ z-WF+9+STPv!sMTe=cn#N)o(y~94|E6DnUT$QSNZ}!|`Z5Neg!6V6pI+;@my-qCY=Q zYBqb{P#u}P$!x4qLPIjas_49d`vFpaS1v}*F$Iy$#ra_&x05PEEF z)-GKC4)Jsa#S6*SmeQ!y@U~p~3x!y>5$W?R)?`IsHEYg#UTA)jo{7q3mQsoNK# znaqbFMmc!5M>Q&l8TFzi@dwfO2u~?Qj7J^`H!~H+=^tW=S02v>P?p%FnT3e6l{SoM zj+>DYWJ$;G>^}paJtp%%J*38;Bf?j+pLpJ7T2mY972i zJqgqrsCE{YyPoc(!J;0KrkptoDsZnE!;21Wwe3lBo6-*;GgEJ&6@9aPBwlCMtX0he zYAjeFNUk(#R~T~3*wgP`oFdY?yOA=q5_DN~ESkJ|HK`s!Dd=a~J}e7{t4Obqci#`& zM!Iu5@?QpUSzZS#L7WfoI4ev8{tzX8Gv}69(5|cLgF~Toy$gmlHRdBVw(^eQZuNw2dwC#e`mTn@#y-PVCZfqXCHHDMiSR@oJP;y`1<*@t zFuAtotJ@~cehY;g+m~iNkx3{(fSObD>F)iH__+n=VHj6c}_G7mZ3!$-wSX=_X)ZchwpPW+~%T}3Lqergf+)j$Fr;qNP zE@?Enyuev6$4rg6o^2B&nU<`#w#zxaoqP8y2iihGb_ejHoef8!OzdjD4GXo|_V}V> z;x2s$%I)bZpT#AZ{3wl_JmnFS3NZC*MUWveI^Au1Nf*v5-?WS6X*S?&I;B*HB9Xgo zA630(H3XJU`4EkdS0EjvC~hBta~^oHpf=X?ssW^6ZCwv9q5LHj{KdJILqEi9yt`u8 z4;~BycnFt7e9kX{`?Nf{%J6mEkmY^W-Y_mRq4RYGl?h^A@zUA%oDS+xX6#sO9eL>v za96z(9YWvIDHKJdwn%|OMmaZ~ikgXj<&@W#&P4vL9r7i|codvmVinnjqqsEZ6xy0q zrM_M6I8Acl5QH)o`4*TaCwyfq5!X+U8b@0o!8Z+G@5XUJMeC$3WJiI9-LEUEYw`v# z!?>%277Bq}-5W*7;@Tw);090V&>x21%2Q?xje8b$0lR9Oe7EzEyq%(xNvjHShSbWn ziy;Jv>nT_R74TbU2CUOqFHC>6YDUFShVL}yBLVUFqjXUwi!Dr3Kh(h6D21xHW;s|T zorKQ4&DBHsrn|Ss~S^ z1}!XNn2EMhzR2|$prqS;hP@Z zq!wDYx$~Z?P2h34y6yETio2s0-<*rZ3Zn^{`m<6z1XxKu=(1ZyHWw65B{rZ0u$ZpH<@-WX+(=dFG458oU^R^@s5?kOHpb=%rt@A&Rm! zUyUkAfxa!uVWvtDF&OP$FId!n+K$IWa7kmIlH0Y!f5!kcGqTB7 z#wCakUE2UI8loo53%8Cr>*9CDxw1z7lqX0O^WMFzw=)mqI_}!MbJX@e^LZV2KXQW_ ze^XC5@UyyovCyF)q4>_lyhr<>-byo5a4XhTy_Y5F-hQZE-B&bg z)|B2H(v!MZsYTLRcH4(&w?1`n(>b@%()?v?)$*+TyMHwW@SaU+`s&mjay?H19}a(X zI}ny5GJ4datmwAwpH|hTN7QW|y)%ZU z(~IKZ#Sm!^|LWI-s;1?|?t^2if^ci&74q*2A%G+*{5hWR1^>l={kI>**8yM9hqVj5 zy9S&1mT#ika7D1VP9e+)fj$(jGxarWZKx$qbqwGhA7V>#HsC(rhxg`rKg0-|77v8 z&3H5f2HZRUih}oDugp6B$Q1;2eN^gGG*J9Z#X6Yr&RaM;9MhK117LU1k|Hx@MTab& zh_=pfnj~#DW$K%h;^67RP}JlkvNc%Ilk`1Psxy(hh0~nRmII`oMvIJ-|5z4ikMsTT z=SS0kfjXC-wO>i-$qU=3A>yfNxX?kq!XF){VimY~V*pC74plAXiKyRuN*m&g=GL+Y zNC~M`=UuZ|Hz>dvzeRhf6y_RfdehAvfAScw&DT)<+KE$3Jb1@=N#K7!B>5;bPf+4D`qR>dk}lc^};FyBz$GJRE-e(*dayqh{Jww>+Az((NZa= z#%FODAD}hXDF1|zH2xNe{->v?_=jOD(Wg(T6cj`vn&iunF^zik4|^{W?=i%%)U6rywS--s``T-q z$NMdkvx@ty6MxSE7Q;@GY7%9|3RktCY;p);#2aVz2jb$FHy ze58!je~vS@(2QJEW5=3(n|n2={E<;e>>u9vRy+a??;3!XOiOR}vg^9N@}oXhznV=E z|F%?^u=V70YDwQT-k!AFD5d+}UmQI|$G6<*t61Lq^{B$QJm9QZII!O1ck8zO+n+1r z0;RxG6L_;vhEd-kfP{lwq`WyYSg(UTaKqBg3QIIFU*l!R!8KvW7r8`%j}OJT!wm2f zMd|7Cqq@2R8a-6nmOk_G&Bs1Vws`s4qM_pr=`$gI3??>*ey>Ux8Ti>l+ovM)g42Qn zN=xQ*IPPOVUyID(rAVvC3dO|~4ED}T5&6@r!KXw_I5>Xa`)93%uHYZb3 zR`~bXJxS5d-!>X}8is33(LRXsOUUQTG)>AxKxN@04xujLFy{YsFu3im?OGE&PWi@N zvOu69e?KOdJRm(ieYiV@Iwts9@KGU3!yg1Ip#SyjK0#kTOmb8{eKv5ayta1P#JLMv zvj&YjPk&aW&4wY)YrHJQN3`427cnyxJXw3w-pHiF=LC#v* zZF1d=6vlq>`}p?J-?Y?U-5fx$T$7oEv8;tMeijxU2M~E&j_f{kll82@(+1h6FrN=- z)@P?dS^nMI;8p>vs!1RbHH$mgAWs80r4uKf^A@3Q#kOQ}NQUmT2L2FC!a(4Kg`J%p zpa2Wr$`{0E#1HS_k`i8U{qP5o(Bp{uk0aUJE%t0Lub6^@ED^045!Bco29EO0vHmI| z>Jz<*BKd-BFPduibJ@h}YuCiTaEHYs*~B{mF#`#Yn!UL|D;d#l15ed03;8S-6+(0s z*Uni$nMv}1N3m-x3&$s(mAP3pGB7JE>zn3*p&^-~*ir-}9xY{Y@&|c%R-6!SLd5Jw zJ6=G%|FpXwk@~*PncnFu0(;1pW6SGv9cN8}gueaAsu(dW+72NiR;28l-FIBB?7M}* zG#^yRlN3J?!c8zWCEI=t=s+U@e;(Xt`=DQOvff{-)Z6REFxCLZizZBCEBG@b+<^?* zr4x^}{XENrZG6iB^J6Ky?hrO&<3C@*M!N^?%6>w4>KIC=_r5#~48ZvAqv8>IP&^@c zA0B))iRa(T*^S#)qU2YzSZZ#0U+*_$B(;n9J@^<@h=~xM#}aq1yZ_PLS4TzJuKi+* z2&klhgn~$S4kZXkcQYW}-7TUbEiK(00}L?KfRsp!bPb)u07K_+9(?z^_gQ;=d!2RG zcg{NJuV*nZ57!;PD}VQOhlqJ1Ix0q{w-rYT{opW8%Nbn0PnNbIIsB~|5A8?QG=BkA z4}j6if1zBi$M!x93qM{!DLZ#K#92n8*Mu3x(^azhXCdoI*~LB6Qxz>@sqrjP(nCr) z=s&kl%o~5dzi=8Ohf|zeiC!REscA9t^{spJ^ZKBPqY8CXDrB|Tp%xY1Y&8^=vxTcz zP}cKY?vEt;x9@*k8`O!uJlce-R?vLc8GYn`Srw5tL_y%jU0)s%hxPs;r5m&_Dh+|E(iFI zBftNFcbbC2t$VmnSR1#_S66pht_;@gdM4g|CkhNLF75%q{`&frjtRXtU9)bU-D9Ah z-`pUO#*G@b3HpREFN27vTb^0>D%O{mXN&+?ym_;{PXC|oK~^~K*yy!;@JG!81Nd{Y zML=A` zwtx5@l#DL^+(u3^MQ9Z=JxqF#J6Jh_VUU--x`^MUi!aztaiNgKv`;Dc;mG|25hao9 zLH+^*h@<|Thj8~O&R<8u<6ke?*D<^?nbz@sw-&90u!EZ@5OF@2s!AN4__M{=1MYtm zUi%kBdrl19TT6*xWK#pgK~f5`V-%AO1$_+H?-sji7yc;xQdThSRib)L_|+s}`D;$? zZaKZX$R^@)%`eMH{$nw))}w%TuDS}Q+;$Q}l9sF&44tGU({=Xgr&6l#4uPg4ovxE{ zSnn~RViPXTRZfCFR1N7&*Io+NCDHMtBYl@q zzG-)lr_eJYFDv|7%rtf1H5!n(u>-1X&W zs+u!^Ydzq-&0j*+1~CNuqL3s9q4>WB)gnzTfI>{TW8qa)(~3H4Q7ksz+Q}jN-R~7p{I!qr)Mp-a^7YE+2 zYv@<_>tHk?%^92)^=G4I-uYXV9si>s{`|lFG^pl&-TIpBhDDCW^^%mUoZ^4IeGu0| zb5ty|5j&hO|2Bl`xjLmV+79YS1CY4k7tNo>f8Y}=e@EJ z9-0r%m}h1vMHxtfXTRbN9Q1WlN#%7h9rgqX>cQwQ9j)cmRA18O$&)7XzpTs8{N+7x z2=@RFCT?XRrp|>~)`U`q*1T6T4>g^yW=w%d0{Z5La30Tqfgs;h?Nv$>o6BwR;<6PC z!Q6Z}IGYzCZaDlgik!w2;$`IGl=HH$xV&N_l`_$$obl%*c*Cpw<@ZOtC)%*%mRC>P z9=2hJyz2&VOyy= zWJJ)IUiYOdaEIM{$YMus0e3t7U?>#jiGssk|Ip)*Fy>cRv!Pl0i7h6#ai=H6(}`TSS3_hvy_qKGhGC34HL&|iivFa<2;(2 zoKDr@%7nKs?sn3G6QAVaR2AeaJ(z$(q|{#x3qzP{2Q(IKJe@Z`@=J)Rjbi%Pj)-Bj zqzphr9mm`2V;gT{tgWwV?Hl*Q-PEuq<51Kv%ugz4vl&vZ2N@%|=I3GOd3#N6|hoL?%?+93q-x*E9n;6`@ zr$Z*dN+#+VkbdF7|+h&mOG#m-(oR{Skx_;uR%6UbXyz;`Rv>A zB-brJHr_~QJJS3do|Mcf_}w6RzcbQrR1wm@TX(E?LjJ>mh>l{a5i*5nhqFo24kiu(?x?u`W5yg09215|@Q?RCtA`TTf6Tm})% zWtD~bbv8O!)(sWVP&HkwK7|fTSuV?t7nPDGRIL}p=8!!&UemBtb!f0t3}5y#?hW0~ zk9yP-iZW75MDSV^N(o#>$S3hy>{+H2=aChFL!J|-%-S=tGP5`+Ys$zz9P%{KadYP0gXv=ZHTduTGWgrAZaX{0uxNoECR@ zQZ8&Ny*tAyd|t31N=`Ne`-$-Qgi-8tDY8CH=rG*Z9WD!&n<0-eRJQrXe|{Do_XP|8 zl6tD7M{c&0Fj1o$+;mr)h#Qj+a*%mgD3oeQz`Zb*#}#~zW6)5JOxvoY%I{Bd>i@pE zn4FobmoQh)JgV&PcqL;sbKxap#OQFnK(h)%cqvZ%G2MYYbi6)F$IXO1Ub@-&Ro|aS zrlG8(=Kqm5L$LjwH`_lD*_?oZh3*eh=-!G?^W6XVdN2C9h58Q^tltCfBxF~!wuCAz zSN7SegdRI&90>vXH7fXV8|4$88`;ouy4}Lu!jIUMt+IxKcAqAFA{k@1R(yEN28*2g zkNEOCY#xYsyC9ub`q)}tI(D|5I2QG77%i%#sEYSzDIo@#T^>kp=7Q+EK3Yb`C-F8} zTB||f4W;Xz;GiK*V-Z~jQmND=m$3%3f`N7oRqe0Ju`my%^iHpL0g*o#_<~>cYEfFY zG)XR%73D`bl!}&`mTrB}VY27(M8&phu>~w{CIpNu+xbH442pEu!>`!!d59@DE4oo$ z>(9>xyjRu1A-{`ohvIB<5^!x;DkMbjPqWaQ`UE|(jPim1a(&kpaZCp00@O_Fm`5!o z6m`BHakH_X$Qf_)9&5%t*|Qql{a0F=34X3HMnPfpx9jfRH^& ziFo$fQ&+OACWIPSvCy`k7BR3oahAPZ4tB!JX9hXsxHc^yJX_uUN*x{gofBM-0(wb5 z_)=_pb*W`z6g8Y7xeYF;BLoYt7;7_;_isn zp&*oEmd;W2X!_Cgq{HoCXbJKNJP3Fo7heFEX4!S>&JY9zWtQyEG|G|wBJHeC@`Mqp zCk?cy<75BH?uZB-^UcQot-g^fwl+y{uP{!`GLcDFH6PkqKJu&V(&eZDSb-FlbLZkG z7FSyfCol4Yd|dFBd(#uNysU~96jgdVg<=|Vz?d?wf% zQ71){37jeHx&@Uxy$tRHlmulp+#R?iz3#*#?#??FiFj?p!5?4Yy4Dnwx)DuRUh+EV zp`r`Q*Ebvd#`XbwEed2u@p&|fG(t9BwDWv2_krrC?pswd(z;ox+twh7>uPZrhbmsP zo-bxNm}|lWSnw#8jIuXNUzZsyF%Np}i#oqi*sbX3h9=OFT^;W<4jL3rx~O&WU@yK` zqlW{zq&Wc_n}lBcoxX4?5iiTHo(HZf1HANu;=N3RJV&@Iw9PWV*11^(bO?DC>-jo- zjGpb#JfwiW+aNR=%=a+#8-Gbg!kn>fTUKc@eyB_Dvt0Dl;HVi$58pGqPr$jV076Ig zcSa!aMqGGH5AhVn1zf1qAs5MPZqq((Ivwn_);V_nvp#gz2l!Nzg_3{Xqes-<4ROPA zYJURYZJ)L{=f!q9^i0{4TkX=XN;F~JH~WvsbSOv(9_K#km>(jD*me^>mT<7GN-UF0 z6=Ll(v`XjTsWg)iau%FTsx(t;_)V(v;?Aq!kM;6T_oLgSrQeG8j(M#&Adt)DyzTo4 z*z|+2W4qb74T@oJ2R%Nru>`{C^-WbeyavieV~}dGFWqm;NsnJ5{AS|S`)?JKw%`uJ z8y96+eKl`^Lriyz^-(egu5(p{K|hu>eS{DLo1x!(A-fg!$7x!F$3r*pfq;c5^~1rp z%ZUO9IQ?J}d@;mpTZKRmy++RGoAc1d^^0zQt>3i!n*qJOciC!RU5zIbug=r^C$|pR zRqmtJqt3gZ|K8rKX=L*84b(Bw*l_A!%)?i#0d-O<%#QC6j_`J6z`rHt`$O$44iv&D zDa;%k1?`CCB9`+sP}?-NuTD49u7opfM>gahu4MPk(%UyL0Ed;+N~_}ZY-3<#K4!7P zw&2m`DBg!}l^WefnbUkt$Fn#EW~Q@`dfHT-5ZM{QTURXs+XqN-6Mg)IYI)rgXjlG} zt(%5M)vU1bnEpx@asV0&TB(1EIBtekGP^7lJeIRSd{sWxjLhrBwb9GdXASN*#JHIK zbbAjyfgT?T7RkxV$arXyaT3kMmOKAqTjJNZ8iqK9CTypQ_BdIP{4i$PHIZV=D zrP)-1P?WV(OK5jZEeb*1Lpa@GsaqrJ!rNdw_7-qsbtcoFL_a@O_Dl9RPbmsZg}a7t zc8W=STQ03upG+C#sEm5$CVAT`F81tqI=4pL2izM2#d|Jg(@VABFaF9_+#hDapZM8V zYRz8kj~}cu)R7WyZf9HjoGUNXBhN}>h52#-`)EU?!U_@DSJ&a-x0?LTksr{Uq%+iR z!wwRq)prmqD67KAe=-o9bLmDkW|Je`4R*c$GJ)^K?q_6$B>S(G)Cge{!(}K)`RIhr{ejQ{ts+J9(}@ORks-+adP1iYu6zc|@=AuWY#YhN+- zOngU;-1tX_lq{~MP5bdP^9=%G(Q0eN*Qr~w8sfDH@cOhutH<@aFL7sf~TLGU0t+VCEbbrib7YUhO^F3uGMCY_i%g`HB1X88# zD&l8)3h92!Av;E8yyLZ5FDkr0|7JGnaFpX|RP*l4K0X_}tOeg*YqfoKhXNlOLrfJa9))ZtE>yaCCIQT*b zwKBwK25_tX91m2Ssf!W5P3*r@XvHrcR-*5=R#^ogwm)C2IH4;y(4SEmdNcbY(K1$2 zHOgtQS@jpGqBX6gMI>bV20ECZ-eFFihKg`jtLsi4SO)?(jg){TVN?12?sN>eQZyK> zIUc8wBBGD(B1!0AF^Fke8lNuvQ%5i6gLrYt1>PW3@|g!XVzLZG@4JHnFsqAD|Jdi@)2lkY&B!t3_bmjUu>KU zWH~MGfV14Dhw7G&9UXLGTvMalYumvIQkwj?MVDrRsZ!}zn$Kngy?S1h*vpfam-2dV z$T=VwLyf;84`W1|gq+5aO7M%CLwN`7evqg>jBaq#lBLXa>0UzJ({Atmxr__F@=_kf zCVzWx-`v;83gT79%i59y@nt7q+UB#RskdnCOnWSYsK3DizWE4}F{s#?U}gfMqS6R+ z=IrAS9#^)TM3hJk#@}u=RK-DKS9*J$?s(|H35!}bTSY}m!pE`QG{Z~|LgxmwzhY)? zDQ!Crs&%pYZCcf|I~7xO?aevB9&9s)Tf>op{bQTk0h|&~OoMZ&65|p$R^knXst{W= z1cLuY8l8%;kmcx(jv0*u8xv7DBaZY1Xy`;(Xk5n=9s~<=(tw{*RWIB3?pHsyrrQ$E z-d6T$lxlEZ|MhKR!MkK{a6m#Y1@Uzhjm@AI6D*4Pjx$-7Qg2&LD_~kus@3>>(WNCV z8^X1gOcC8ZPOl^z>br%p@HqT|^%9s8#}FX>NU194O}C~a7pXn9ocz)zQbvpQ`uP9{YA=>sc*^eEa5MP zjCB39Qk-MpaXGdAy8v_4Ar-#65CQc1y7=%3?X48HVL}4YFMI>aG99OX_T%i}2dznk zeii*3&!kcJR0120sl!qrg$$8jpX4mHehHt!@J@}O^z$aQ+beOCZwc@E9b67tauWaMY zXPz7{4lps(iQ_(#ANKrAp(Z7nX{6PhHjm_WR&C0vr0q3zbC8Io^RTf`qwZ9c`DLRA zA?fcqa1S;mvDx{MfN(5a%7X^6UNT-d#JWRFlX5~$ne2PT zr3BGUH7^~U+FM|CQ7{zRzHTH{x9r$$i7!w}Y<9n&RA7eM4kB>LPpoxY^+z+a<|IwE z$g6#MUe^}qJGn1QypU%ea=8f=%Y{Exv(u*}i^33qONHBe-7i2#!`-u+ld=b0@ipgc z#5s~oI!Cp5VE@Bt5}U|DjKFDkB8J7iwYam<9}OGx&5M+e90f5az3eT*QD8Zpb}1vE z@1*ULDpq?wD!c9y@zg!%DBYCHO>R+xJRH3_b{O9}K{jq#x?EnUz^3zG^G3JNiLP^! zu90FB5{GmkXg^N)k81K*J`ot4PGnjLZ@kziY!X8 zj(uWY{ZFQ4?s|lsnqhTT>OY`Y#eM&^2+1T9J(#t$Bu` zyTwR`9P4&Vx?>%fMarP9nB?}V5HgpcGPXQ6JjT%$=?)p}MC&(#qKos`wJ>glbP~Ck zndt>bGwT)6MuWPmNoF8%;rH>iU5z$HG|oV|CQAyhP~&A(m4r>7=ln)jelAODmH+s( z`4gIJ1mXcFHB`laGNr_%>+!FkJyf+Q`nW*i1V1Dh+qDvahBPY;ls6b%mEczUcxpdb zx{rk%gLeaUlAXJR)t;OAjs18=H_ITIf!TH0#IH?!W6u0`6eod&9Lkcm{9Jk=KhZGb zr0e`i0>Y;&T?4W_)@Vl^KH%j4n0myz?p@P(uNy24zgfxZjtOUeIN}=(jxIMhZ3eD+d_%@XSLTMulG!p(9*%1q!*|)_ zqJHF?0)#^m=xC{;?%sr2)Rk{&YchO0X|$1fmMPa%ulB`40+F2Bi_4XnngCCfKK``p zH&M?{9$riRip|>FG8U2(uEbtNqF?=Dlx!m@)+Uh0}~kSZ9VrgFTciM!=b{-8uP;hlMha-eoht;T=IiH4%4$N3vO#&LPf408&eo*% zGq$}v!yZ|w>-B$1*shfdH9SouS?z($9f$4LAyh-Zpmb2_^*$0$?YP%GbTcy$q$RGp zAw?sygxBtAL#UjT&+FJ4+BwF9gngENn#5%1Pn1sIJmAMutD8l|@puQu9@ zSDO&i*VTphQPhtN7)@=8Hr~dr_}=}we8B=)y=I?a`QAwkzZM-hVwS~u{lR?B{g3~9 z&l97wbn=*LYHt60Rz?*9Q@|CyjGr|xVi>4g_VayyE(}1lx)p!;_~Th<;L`Sq+|QpO zH?igyRg$hGw?+L|3rhf>|1-+_&vw=(y(Yc5~?b7HO<37H-4&cjZC0fzieR0x0 zg9nwT>UvpPjP>FS*7qp+XFri_=~Yj>T;|*^14=!}bE@X6tkZi1(2et*lsgAsuASCQ zP#Z*tOn7BG^fH6O7irK1BW~TemN02J%U3_6?_3$gZx0ajK5zbZh@#puMfdve5k#1O zSMuMB?PYD0zYYg_?$zu$v4k4CxIlN?@ozRnv57RYFdNnG9S_+B0K_+v_{Bj?>BTca zd-t`l)6=3m0B7S<)HdZ(7UwK6p=b_v&F+if$YzI_(Nr*%m ze8R;h>^5&`y$ZPcH9`&4dl~2Wd`m9;2BamfJ~Bz(m^W^h_K1r9bEW;iijn_boc!0% z=Qpmz{4!gD$AN$NEMpg#n23zDbvuj%B#n=S@hxGhVyl6n-=5>ZwnGmR&PFL zdc%EH!NJjnxB_2JR!X%7Ol%E98aqX8f43z?kJkI_lLal6Cy9;(T!OsP(s{VGNLJ0X z(n?qQZeleoeF$hfmo{+m`=K4LaQ`9RkrpPZzl#?u69#nG5QD;xIx2JMBE(gOLUj8V zu_YkNL>ktgJ0kFe>gUua*Uy?gCMHN1CRAtySBigTGLgkX;O3c~0*!itUhXcH677uHt zbF+e*(lhHqvSsOt<4CqS%ZS7~>grxc$jbIH*`++Gv8btkwPR_wzY^3{KpR1k>!!O8 zgxluYjV-1qUOfn`99{!+6*ljRVVtgL~PL%%1szUeTdj4XNRrN-y9ocw_@J7+nh)MUuw z+c#~Ziz&k%QNL%c-m@VNg;v%t)r-48)gBR*VJqjU8VID>eSUPO|7!;_-v-Jw763*q zyveD=nD!E-EBct^WD1Ty9@01JDT~Z#zQ8cy;IHk>c@%yxM^ESm_DbE*|G>IS4)nGYV zHM0LTcz|oDR?Kv_YhB}T^ibR-r_6wef`?z3uJGW6sy^S6msY@Hg$9|`L$|rXf(XTQ zVY@g4QX#Ov=2Nh3AhryLR$XYM+|x1vBSB2rFC3xT$$(!I3~F6?uCt}zV%3!CEa`&t z$jk1$m+Sm=^6?&4CWJ$L;(Th>$e8Wid+F+#^K6rct&)2uuFH1IP6H#dJ2bW?P1qx^ z{%AfC6x`Refw*0eiTgxp3$YuhDqaE0E5WzDH2u|7-%F)_lz;i=2&mave=<%!mDGO!L-fSX}DXc#T_lj;L2)$$`pOKx+pa~nIp`~G#gT82aS z2A1RFd0;vnHy+2&ix;A5S4+1NWt;Cn;qi~3MUTd9r)OqC& z>4H@pU}^lCD1I(9{f!|f@A}M>+_Ktb$0Feo{x3}7SeY4%*W!oJJUSAwuv;Lx(%)|t zQRs>D^dm{9&wO$zsbKlWRbJh-aKA&urA!TwSMHCz96&AeJk=dw7&i-@$2R>W|NZ0H zRSSekaE9x7E%I=urKG$ZA_`oaOKtxtL9Uf-CMW{azzs6N9?pi5DS zVmZKE7ln=$pXp1maLuSLztdy2WS3gr|;PY;zl3z`E*V;m(cXtc$Qd zMX@C#OL_%9- = {body};\n") +STUDY_SEQ = re.compile( + r"N=\s*(\d+) causal=(\d) flash\s+dp max\s+([\d.]+) mean\s+([\d.]+) \| tot max\s+([\d.]+) mean\s+([\d.]+)") +STUDY_VS = re.compile( + r"^(flash|naive) N=32\s+dp max\s+([\d.]+) mean\s+([\d.]+) \| tot max\s+([\d.]+) mean\s+([\d.]+)", re.M) + + +def study_numbers(): + """Rows of the recorded 1000-seed study (sim/golden/study_results_1000seeds.txt), parsed verbatim.""" + text = STUDY_TXT.read_text() + seq = [] + for m in STUDY_SEQ.finditer(text): + n, c, dpm, dpa, tm, ta = m.groups() + seq.append(dict(n=int(n), causal=c == "1", dpMax=float(dpm), dpMean=float(dpa), totMax=float(tm), totMean=float(ta))) + vs = {} + for m in STUDY_VS.finditer(text): + mode, dpm, dpa, tm, ta = m.groups() + vs[mode] = dict(dpMax=float(dpm), dpMean=float(dpa), totMax=float(tm), totMean=float(ta)) + return dict(sequence=seq, flashVsNaive=vs, raw=text) + + +PERF_KEYS = [ + ("FLASH, Q block 4 rows, no overlap", "flash/N32/c0/q4/o0/L10"), + ("FLASH, Q block 4 rows, overlap", "flash/N32/c0/q4/o1/L10"), + ("FLASH, Q block 16 rows, no overlap", "flash/N32/c0/q16/o0/L10"), + ("FLASH, Q block 16 rows, overlap", "flash/N32/c0/q16/o1/L10"), + ("NAIVE", "naive/N32/c0/L10"), +] + + +def perf_numbers(): + base = json.loads(PERF_JSON.read_text()) + return [dict(label=label, cycles=base[key]) for label, key in PERF_KEYS] + + +def preset_errors(): + """Max |engine - float| per preset, in float units, on the golden model (the same numbers the visualizer shows).""" + out = [] + for make in presets.ALL: + p = make() + q, k, v = (np.round(np.asarray(p[x], dtype=np.float64), 6) for x in ("q", "k", "v")) + n = q.shape[0] + qz = fixed.quantize(q, k, v) + res = fixed.flash(qz, n, p["causal"], qblock=min(4, n)) + out_f = fixed.dequant_out(res, qz) + ref, _ = attention(q, k, v, p["causal"]) + out.append(dict(name=p["name"], n=n, causal=bool(p["causal"]), maxErr=float(np.max(np.abs(out_f - ref))))) + return out + + +def measured_ts(): + study = study_numbers() + body = dict(study=dict(sequence=study["sequence"], flashVsNaive=study["flashVsNaive"]), + studyRaw=study["raw"], perfN32=perf_numbers(), presetErrors=preset_errors()) + return ("// GENERATED by sim/golden/export_web_data.py from the golden model, sim/golden/study_results_1000seeds.txt\n" + "// and sim/perf_baseline.json. Do not edit.\n" + "export const MEASURED = " + json.dumps(body, indent=1) + " as const;\n") + + def main(): WEB_PRESETS.write_text(presets_ts(preset_entries())) QUANT_CASES.write_text(json.dumps(quant_cases()) + "\n") TRAFFIC_TS.write_text(traffic_ts(traffic_entries())) - print(f"wrote {WEB_PRESETS.name} ({len(presets.ALL)} presets), {QUANT_CASES.name}, {TRAFFIC_TS.name}") + MEASURED_TS.write_text(measured_ts()) + print(f"wrote {WEB_PRESETS.name} ({len(presets.ALL)} presets), {QUANT_CASES.name}, {TRAFFIC_TS.name}, {MEASURED_TS.name}") if __name__ == "__main__": diff --git a/sim/golden/test_golden.py b/sim/golden/test_golden.py index 896842f..4e00af5 100644 --- a/sim/golden/test_golden.py +++ b/sim/golden/test_golden.py @@ -278,3 +278,4 @@ def test_web_data_fresh(): assert ewd.WEB_PRESETS.read_text() == ewd.presets_ts(ewd.preset_entries()), "presets.ts is stale" assert ewd.QUANT_CASES.read_text() == json.dumps(ewd.quant_cases()) + "\n", "quantize_cases.json is stale" assert ewd.TRAFFIC_TS.read_text() == ewd.traffic_ts(ewd.traffic_entries()), "traffic-table.ts is stale" + assert ewd.MEASURED_TS.read_text() == ewd.measured_ts(), "measured.ts is stale" diff --git a/web/e2e/docs.spec.ts b/web/e2e/docs.spec.ts new file mode 100644 index 0000000..661e30f --- /dev/null +++ b/web/e2e/docs.spec.ts @@ -0,0 +1,97 @@ +import { expect, test, type Page } from "@playwright/test"; + +const pages = [ + { path: "/", h1: "FlashAttention, built in hardware" }, + { path: "/docs", h1: "Docs" }, + { path: "/docs/what-is-attention", h1: "What is attention" }, + { path: "/docs/online-softmax", h1: "Online softmax" }, + { path: "/docs/flashattention", h1: "FlashAttention" }, + { path: "/docs/fixed-point", h1: "Fixed point" }, + { path: "/docs/architecture", h1: "Architecture" }, + { path: "/docs/verification", h1: "Verification" }, +]; + +// Collects console errors and page errors so a broken island fails the test. +function watchErrors(page: Page) { + const errors: string[] = []; + page.on("pageerror", (e) => errors.push(e.message)); + page.on("console", (m) => { + if (m.type() === "error") errors.push(m.text()); + }); + return errors; +} + +for (const p of pages) { + test(`${p.path} renders with no errors`, async ({ page }) => { + const errors = watchErrors(page); + const response = await page.goto(p.path); + expect(response?.status()).toBe(200); + await expect(page.getByRole("heading", { level: 1 })).toContainText(p.h1); + await page.waitForTimeout(500); + expect(errors, errors.join("\n")).toEqual([]); + }); +} + +test("landing page links to the engine and the docs", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("link", { name: "Open the engine" })).toHaveAttribute("href", "/app"); + await expect(page.getByRole("link", { name: "Read the docs" })).toHaveAttribute("href", "/docs"); +}); + +test("landing hero animates the tile schedule", async ({ page }) => { + await page.goto("/"); + const grid = page.getByRole("img", { name: /FLASH tile schedule/ }); + await expect(grid).toBeVisible(); + const before = await grid.evaluate((g) => g.innerHTML); + await page.waitForTimeout(700); + const after = await grid.evaluate((g) => g.innerHTML); + expect(after).not.toEqual(before); +}); + +test("what is attention: the demo's probabilities sum to 100%", async ({ page }) => { + await page.goto("/docs/what-is-attention"); + await expect(page.getByText("Token that asks (query)")).toBeVisible(); + const text = await page.locator("main").innerText(); + const shares = [...text.matchAll(/(\d+\.\d)%/g)].map((m) => Number(m[1])); + expect(shares.length).toBeGreaterThan(0); + const sum = shares.reduce((a, b) => a + b, 0); + expect(Math.abs(sum - 100)).toBeLessThan(1); +}); + +test("online softmax: the engine records a run and the row table fills", async ({ page }) => { + await page.goto("/docs/online-softmax"); + await expect(page.getByRole("cell", { name: "kv0" })).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText("m engine / float")).toBeVisible(); +}); + +test("flashattention: traffic demo shows FLASH and NAIVE byte counts", async ({ page }) => { + await page.goto("/docs/flashattention"); + await expect(page.getByText(/FLASH \(Q block 4 rows\)/)).toBeVisible(); + await expect(page.getByText(/NAIVE \(N×N scores in HBM\)/)).toBeVisible(); + // at N = 32 with causal off, FLASH with 4-row blocks moves a different total than NAIVE + await page.getByLabel("N").selectOption("32"); + await expect(page.getByText(/FLASH moves [0-9.]+× fewer bytes than NAIVE here/)).toBeVisible(); +}); + +test("fixed point: the quantizer shows the INT8 values and cfg_scale in hex", async ({ page }) => { + await page.goto("/docs/fixed-point"); + await expect(page.getByText(/= \d+ = 0x[0-9a-f]{6}/)).toBeVisible(); + await page.getByLabel("q dimension 0").fill("3.5"); + await expect(page.getByText(/int8 127/).first()).toBeVisible(); +}); + +for (const p of pages) { + test(`${p.path} has no horizontal scroll at phone width`, async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto(p.path); + await page.waitForTimeout(300); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(0); + }); +} + +test("social metadata is present on the landing page", async ({ page }) => { + await page.goto("/"); + await expect(page.locator('meta[property="og:image"]')).toHaveCount(1); + await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute("content", "summary_large_image"); +}); diff --git a/web/public/og.png b/web/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..08ea54e3980665b3760662b4e743cfc1ca9bd446 GIT binary patch literal 49286 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 literal 0 HcmV?d00001 diff --git a/web/scripts/capture-assets.mjs b/web/scripts/capture-assets.mjs new file mode 100644 index 0000000..daf5f97 --- /dev/null +++ b/web/scripts/capture-assets.mjs @@ -0,0 +1,55 @@ +// Captures the social card and a screenshot of the visualizer from the built site. +// bash: pnpm build && node scripts/capture-assets.mjs +// Writes public/og.png (1200 x 630, from /og) and docs/media/visualizer.png (from /app, for the README). Rebuild after +// running this so public/og.png is included in dist/. +import { spawn } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "@playwright/test"; + +const webRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = join(webRoot, ".."); +const port = 4340; + +const server = spawn("python3", ["-m", "http.server", String(port), "--bind", "127.0.0.1"], { + cwd: join(webRoot, "dist"), + stdio: "ignore", +}); + +async function waitForServer() { + for (let i = 0; i < 50; i++) { + try { + const r = await fetch(`http://127.0.0.1:${port}/og/`); + if (r.ok) return; + } catch { + // not up yet + } + await new Promise((res) => setTimeout(res, 200)); + } + throw new Error("static server did not start"); +} + +try { + await waitForServer(); + const browser = await chromium.launch(); + try { + const og = await browser.newPage({ viewport: { width: 1200, height: 630 } }); + await og.goto(`http://127.0.0.1:${port}/og/`); + await og.waitForLoadState("load"); + await og.screenshot({ path: join(webRoot, "public", "og.png") }); + + mkdirSync(join(repoRoot, "docs", "media"), { recursive: true }); + const app = await browser.newPage({ viewport: { width: 1200, height: 900 }, colorScheme: "dark" }); + await app.goto(`http://127.0.0.1:${port}/app/`); + await app.getByRole("button", { name: "Start" }).click({ timeout: 30_000 }); + await app.getByRole("button", { name: "Run to end" }).click(); + await app.getByRole("heading", { name: "Output" }).waitFor({ timeout: 30_000 }); + await app.screenshot({ path: join(repoRoot, "docs", "media", "visualizer.png"), fullPage: false }); + console.log("wrote web/public/og.png and docs/media/visualizer.png"); + } finally { + await browser.close(); + } +} finally { + server.kill(); +} diff --git a/web/src/components/docs/AttentionDemo.tsx b/web/src/components/docs/AttentionDemo.tsx new file mode 100644 index 0000000..33518fb --- /dev/null +++ b/web/src/components/docs/AttentionDemo.tsx @@ -0,0 +1,50 @@ +// Float attention for one row of a preset: the probabilities over the keys. No engine needed. +import { useMemo, useState } from "react"; +import { PRESETS } from "@/lib/presets"; +import { attention } from "@/lib/reference"; + +export default function AttentionDemo() { + const [preset, setPreset] = useState(0); + const p = PRESETS[preset] ?? PRESETS[0]; + const n = p?.q.length ?? 0; + const [row, setRow] = useState(0); + const labels = p?.labels ?? []; + + const probs = useMemo(() => (p ? attention(p.q, p.k, p.v, p.causal).probs : []), [p]); + const weights = probs[row] ?? []; + + return ( +

+
+ + +
+
+ {weights.map((w, j) => ( +
+ {labels[j] ?? j} +
+
+
+ {(w * 100).toFixed(1)}% +
+ ))} +
+

+ Each bar is one key's share of the query's attention. The shares sum to 100%. Causal presets give later tokens no weight. +

+
+ ); +} diff --git a/web/src/components/docs/DocsLayout.astro b/web/src/components/docs/DocsLayout.astro new file mode 100644 index 0000000..438aaa2 --- /dev/null +++ b/web/src/components/docs/DocsLayout.astro @@ -0,0 +1,51 @@ +--- +import Layout from "@/layouts/Layout.astro"; + +interface Props { + title: string; + description: string; + current?: string; +} + +const { title, description, current = "" } = Astro.props; +const links = [ + { href: "/docs/what-is-attention", label: "What is attention" }, + { href: "/docs/online-softmax", label: "Online softmax" }, + { href: "/docs/flashattention", label: "FlashAttention" }, + { href: "/docs/fixed-point", label: "Fixed point" }, + { href: "/docs/architecture", label: "Architecture" }, + { href: "/docs/verification", label: "Verification" }, +]; +--- + + +
+ +
+
+

{title}

+ +
+
+ + diff --git a/web/src/components/docs/Hero.tsx b/web/src/components/docs/Hero.tsx new file mode 100644 index 0000000..bd745ba --- /dev/null +++ b/web/src/components/docs/Hero.tsx @@ -0,0 +1,58 @@ +// Landing hero: the FLASH schedule for N = 16 (Q block of 8 rows, causal) sweeping the tile grid. Pure React, no engine. +import { useEffect, useMemo, useState } from "react"; +import { flashTileOrder } from "@/lib/schedule"; + +const N = 16; +const QROWS = 8; +const STEP_MS = 180; + +export default function Hero() { + const order = useMemo(() => flashTileOrder(N, QROWS, true), []); + const [step, setStep] = useState(0); + + useEffect(() => { + const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (reduce) { + setStep(order.length); + return; + } + const id = window.setInterval(() => setStep((s) => (s + 1) % (order.length + 8)), STEP_MS); + return () => window.clearInterval(id); + }, [order.length]); + + const blocks = N / 4; + const done = new Set(); + order.slice(0, Math.min(step, order.length)).forEach((t) => done.add(`${t.qi},${t.kj}`)); + const current = step < order.length ? order[step] : undefined; + const cur = current ? `${current.qi},${current.kj}` : ""; + + return ( +
+
+
+ {Array.from({ length: blocks }, (_, kj) => ( +
kv{kj}
+ ))} + {Array.from({ length: blocks }, (_, qi) => ( +
+
q{qi}
+ {Array.from({ length: blocks }, (_, kj) => { + const masked = kj > qi; + const key = `${qi},${kj}`; + const bg = masked ? "transparent" : key === cur ? "var(--accent)" : done.has(key) ? "var(--ok)" : "var(--panel)"; + return ( +
+ ); + })} +
+ ))} +
+

+ Each cell is a 4×4 tile of the attention matrix. Blue is computing, green is done, faded tiles are masked and never + computed. The full matrix is never stored. +

+
+ ); +} diff --git a/web/src/components/docs/OnlineSoftmaxDemo.tsx b/web/src/components/docs/OnlineSoftmaxDemo.tsx new file mode 100644 index 0000000..368ffc4 --- /dev/null +++ b/web/src/components/docs/OnlineSoftmaxDemo.tsx @@ -0,0 +1,40 @@ +// The online softmax on the toy sentence: the engine records each tile's row state; SoftmaxCellView shows it. +import { useEffect, useState } from "react"; +import SoftmaxCellView from "@/components/SoftmaxCellView"; +import { loadEngine } from "@/lib/engine"; +import { PRESETS } from "@/lib/presets"; +import { recordFlashRun } from "@/lib/record"; +import type { TileSample } from "@/lib/softmax-cell"; +import type { EngineModule } from "@/lib/state-schema"; + +const preset = PRESETS[0]!; + +export default function OnlineSoftmaxDemo() { + const [mod, setMod] = useState(null); + const [samples, setSamples] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + loadEngine().then(setMod).catch((e: unknown) => setError(e instanceof Error ? e.message : String(e))); + }, []); + + useEffect(() => { + if (!mod) return; + try { + setSamples(recordFlashRun(mod, preset.q, preset.k, preset.v, preset.causal, 4, 10)); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } + }, [mod]); + + if (error) return

{error}

; + if (!samples) return

Running the engine on the toy sentence…

; + return ( +
+

+ The toy sentence on the engine, FLASH schedule. Pick a token to see the running state its row goes through. +

+ +
+ ); +} diff --git a/web/src/components/docs/QuantizerDemo.tsx b/web/src/components/docs/QuantizerDemo.tsx new file mode 100644 index 0000000..6ac1643 --- /dev/null +++ b/web/src/components/docs/QuantizerDemo.tsx @@ -0,0 +1,63 @@ +// Host-side INT8 quantization of one Q row and one K row, the way the engine's inputs are prepared (quantize.ts mirrors +// sim/golden/fixed.py). Shows the INT8 values, the per-tensor scales, and the folded cfg_scale in hex. +import { useMemo, useState } from "react"; +import { D, LOG2E, quantize } from "@/lib/quantize"; + +const fmtNum = (x: number) => x.toFixed(4); + +export default function QuantizerDemo() { + const [qRow, setQRow] = useState([1.2, -0.7, 2.4, 0.1]); + const [kRow, setKRow] = useState([0.9, 1.8, -2.1, 0.4]); + + const result = useMemo(() => { + const vRow = [[0.5, -0.5, 0.25, 1]]; + return quantize([qRow], [kRow], vRow); + }, [qRow, kRow]); + + const setCell = (which: "q" | "k", i: number, value: number) => { + const update = (row: number[]) => row.map((x, j) => (j === i ? value : x)); + if (which === "q") setQRow((r) => update(r)); + else setKRow((r) => update(r)); + }; + + const dq = (ints: number[], s: number) => ints.map((x) => x * s); + + return ( +
+ {(["q", "k"] as const).map((which) => { + const row = which === "q" ? qRow : kRow; + const ints = which === "q" ? result.q : result.k; + const scale = which === "q" ? result.sq : result.sk; + return ( +
+
{which === "q" ? "Q row" : "K row"}
+
+ {row.map((x, i) => ( + { const v = e.target.valueAsNumber; if (Number.isFinite(v)) setCell(which, i, v); }} /> + ))} +
+
+ {ints.map((x, i) => ( +
+
int8 {x}
+
≈ {fmtNum(dq([x], scale)[0] ?? 0)}
+
+ ))} +
+
+ scale {scale.toFixed(6)} (int8 = round(x / scale), halves to even; step error ≤ scale / 2 = {(scale / 2).toFixed(4)}) +
+
+ ); + })} +
+
cfg_scale = round(s_Q · s_K · log2(e) / √{D} · 2²⁰)
+
+ = {result.cfgScale} = 0x{result.cfgScale.toString(16).padStart(6, "0")} (24 bits, UQ4.20; scales log2(e)≈{LOG2E.toFixed(4)}) +
+
+
+ ); +} diff --git a/web/src/components/docs/TrafficDemo.tsx b/web/src/components/docs/TrafficDemo.tsx new file mode 100644 index 0000000..849faa4 --- /dev/null +++ b/web/src/components/docs/TrafficDemo.tsx @@ -0,0 +1,63 @@ +// HBM traffic for FLASH and NAIVE from the golden traffic model (web/src/lib/traffic-table.ts, generated). +// The cocotb end-to-end test checks these formulas against the RTL byte counters for every N, causal setting and block size. +import { useState } from "react"; +import { TRAFFIC } from "@/lib/traffic-table"; + +const NS = [4, 8, 12, 16, 20, 24, 28, 32]; + +export default function TrafficDemo() { + const [n, setN] = useState(32); + const [causal, setCausal] = useState(false); + const [qrows, setQrows] = useState(4); + const c = causal ? 1 : 0; + const flash = TRAFFIC[`FLASH:${n}:${c}:${qrows}`]; + const naive = TRAFFIC[`NAIVE:${n}:${c}:4`]; + const flashTotal = flash ? flash.rd + flash.wr : 0; + const naiveTotal = naive ? naive.rd + naive.wr : 0; + const max = Math.max(flashTotal, naiveTotal, 1); + + return ( +
+
+ + + +
+ + {[ + { name: "NAIVE (N×N scores in HBM)", total: naiveTotal, color: "var(--err)" }, + { name: `FLASH (Q block ${qrows} rows)`, total: flashTotal, color: "var(--accent)" }, + ].map((row) => ( +
+
+ {row.name} + {row.total} B +
+
+
+
+
+ ))} + +

+ FLASH moves {flashTotal ? (naiveTotal / flashTotal).toFixed(1) : "-"}× fewer bytes than NAIVE here. + Tiles computed: FLASH {flash?.tiles ?? "-"}, NAIVE {naive?.tiles ?? "-"}. +

+
+ ); +} diff --git a/web/src/layouts/Layout.astro b/web/src/layouts/Layout.astro index 77fb92b..30df1a1 100644 --- a/web/src/layouts/Layout.astro +++ b/web/src/layouts/Layout.astro @@ -4,12 +4,15 @@ import "@/styles/global.css"; interface Props { title: string; description?: string; + image?: string; } const { title, description = "A real FlashAttention engine in SystemVerilog, compiled to WebAssembly and running live in your browser.", + image = "/og.png", } = Astro.props; +const imageUrl = new URL(image, Astro.site ?? "https://tiny-attention.vercel.app").href; --- @@ -30,6 +33,16 @@ const { })(); + + + + + + + + + + {title} diff --git a/web/src/lib/measured.ts b/web/src/lib/measured.ts new file mode 100644 index 0000000..b519cc6 --- /dev/null +++ b/web/src/lib/measured.ts @@ -0,0 +1,147 @@ +// GENERATED by sim/golden/export_web_data.py from the golden model, sim/golden/study_results_1000seeds.txt +// and sim/perf_baseline.json. Do not edit. +export const MEASURED = { + "study": { + "sequence": [ + { + "n": 4, + "causal": false, + "dpMax": 0.624, + "dpMean": 0.0665, + "totMax": 6.431, + "totMean": 0.3839 + }, + { + "n": 4, + "causal": true, + "dpMax": 0.591, + "dpMean": 0.0418, + "totMax": 8.085, + "totMean": 0.3209 + }, + { + "n": 8, + "causal": false, + "dpMax": 0.849, + "dpMean": 0.064, + "totMax": 10.942, + "totMean": 0.4172 + }, + { + "n": 8, + "causal": true, + "dpMax": 0.849, + "dpMean": 0.0486, + "totMax": 8.855, + "totMean": 0.3728 + }, + { + "n": 16, + "causal": false, + "dpMax": 1.061, + "dpMean": 0.0661, + "totMax": 9.016, + "totMean": 0.4541 + }, + { + "n": 16, + "causal": true, + "dpMax": 0.981, + "dpMean": 0.0529, + "totMax": 9.383, + "totMean": 0.4065 + }, + { + "n": 32, + "causal": false, + "dpMax": 1.26, + "dpMean": 0.0691, + "totMax": 11.904, + "totMean": 0.4722 + }, + { + "n": 32, + "causal": true, + "dpMax": 1.023, + "dpMean": 0.0563, + "totMax": 13.076, + "totMean": 0.4403 + } + ], + "flashVsNaive": { + "flash": { + "dpMax": 1.26, + "dpMean": 0.0691, + "totMax": 11.904, + "totMean": 0.4722 + }, + "naive": { + "dpMax": 1.744, + "dpMean": 0.0989, + "totMax": 11.806, + "totMean": 0.4798 + } + } + }, + "studyRaw": "== P width sweep (N=32, FLASH, 1000 seeds per cell)\nsoft pf=5 dp max 11.576 mean 0.4951 | tot max 11.309 mean 0.5591\nsoft pf=6 dp max 6.080 mean 0.2603 | tot max 5.701 mean 0.3600\nsoft pf=7 dp max 3.309 mean 0.1352 | tot max 3.869 mean 0.2752\nsoft pf=8 dp max 1.572 mean 0.0701 | tot max 4.088 mean 0.2438\npeaky pf=5 dp max 8.862 mean 0.4016 | tot max 11.935 mean 0.6607\npeaky pf=6 dp max 4.469 mean 0.2251 | tot max 11.912 mean 0.5428\npeaky pf=7 dp max 2.052 mean 0.1250 | tot max 11.904 mean 0.4913\npeaky pf=8 dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nsharp pf=5 dp max 6.742 mean 0.2361 | tot max 21.089 mean 0.6762\nsharp pf=6 dp max 3.850 mean 0.1341 | tot max 20.401 mean 0.6218\nsharp pf=7 dp max 2.383 mean 0.0758 | tot max 21.257 mean 0.5989\nsharp pf=8 dp max 1.046 mean 0.0422 | tot max 20.999 mean 0.5900\n== rounding knobs (peaky, N=32, exp/z/acc round vs truncate)\nall truncate (default) dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nexp round dp max 1.260 mean 0.0685 | tot max 11.900 mean 0.4721\nz round dp max 1.260 mean 0.0690 | tot max 11.904 mean 0.4720\nacc round dp max 1.260 mean 0.0682 | tot max 11.896 mean 0.4720\nall round dp max 1.260 mean 0.0677 | tot max 11.896 mean 0.4718\n== recip refinement (peaky, N=32)\nnr_steps=0 dp max 1.437 mean 0.1264 | tot max 11.892 mean 0.4910\nnr_steps=1 dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nnr_steps=2 dp max 1.260 mean 0.0692 | tot max 11.904 mean 0.4723\n== l/O extra fractional bits (peaky, N=32)\nlf=of=2 dp max 1.260 mean 0.0723 | tot max 11.912 mean 0.4728\nlf=of=4 dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nlf=of=6 dp max 1.260 mean 0.0684 | tot max 11.900 mean 0.4721\nlf=of=8 dp max 1.260 mean 0.0683 | tot max 11.900 mean 0.4720\n== cfg_scale fractional bits sf (cfg_scale is 24 bits; N=32)\nsmall sf=12 overflow: cfg_scale 0 does not fit 24 bits at sf=12 (inputs badly scaled)\nsmall sf=16 dp max 1.143 mean 0.0631 | tot max 1.101 mean 0.0798\nsmall sf=20 dp max 0.080 mean 0.0106 | tot max 0.223 mean 0.0424\nsmall sf=24 dp max 0.056 mean 0.0095 | tot max 0.219 mean 0.0421\nsoft sf=12 dp max 19.778 mean 1.1309 | tot max 19.950 mean 1.1711\nsoft sf=16 dp max 2.414 mean 0.1072 | tot max 4.147 mean 0.2580\nsoft sf=20 dp max 1.572 mean 0.0701 | tot max 4.088 mean 0.2438\nsoft sf=24 dp max 1.572 mean 0.0700 | tot max 3.979 mean 0.2439\nsharp sf=12 dp max 2.801 mean 0.1098 | tot max 20.401 mean 0.6020\nsharp sf=16 dp max 1.046 mean 0.0430 | tot max 20.999 mean 0.5901\nsharp sf=20 dp max 1.046 mean 0.0422 | tot max 20.999 mean 0.5900\nsharp sf=24 dp max 1.046 mean 0.0422 | tot max 20.999 mean 0.5901\n== sequence length and causal (default fmt)\nN= 4 causal=0 flash dp max 0.624 mean 0.0665 | tot max 6.431 mean 0.3839\nN= 4 causal=1 flash dp max 0.591 mean 0.0418 | tot max 8.085 mean 0.3209\nN= 8 causal=0 flash dp max 0.849 mean 0.0640 | tot max 10.942 mean 0.4172\nN= 8 causal=1 flash dp max 0.849 mean 0.0486 | tot max 8.855 mean 0.3728\nN=16 causal=0 flash dp max 1.061 mean 0.0661 | tot max 9.016 mean 0.4541\nN=16 causal=1 flash dp max 0.981 mean 0.0529 | tot max 9.383 mean 0.4065\nN=32 causal=0 flash dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nN=32 causal=1 flash dp max 1.023 mean 0.0563 | tot max 13.076 mean 0.4403\n== FLASH vs NAIVE (peaky, default fmt)\nflash N=32 dp max 1.260 mean 0.0691 | tot max 11.904 mean 0.4722\nnaive N=32 dp max 1.744 mean 0.0989 | tot max 11.806 mean 0.4798\n", + "perfN32": [ + { + "label": "FLASH, Q block 4 rows, no overlap", + "cycles": 4113 + }, + { + "label": "FLASH, Q block 4 rows, overlap", + "cycles": 2937 + }, + { + "label": "FLASH, Q block 16 rows, no overlap", + "cycles": 2133 + }, + { + "label": "FLASH, Q block 16 rows, overlap", + "cycles": 1657 + }, + { + "label": "NAIVE", + "cycles": 10961 + } + ], + "presetErrors": [ + { + "name": "toy_sentence", + "n": 12, + "causal": true, + "maxErr": 0.004894227790511407 + }, + { + "name": "previous_token", + "n": 16, + "causal": true, + "maxErr": 0.010518908053282039 + }, + { + "name": "attention_sink", + "n": 16, + "causal": true, + "maxErr": 0.02534997660881233 + }, + { + "name": "uniform", + "n": 16, + "causal": false, + "maxErr": 0.002772959153543364 + }, + { + "name": "self", + "n": 16, + "causal": false, + "maxErr": 0.02599748051857198 + }, + { + "name": "random", + "n": 16, + "causal": false, + "maxErr": 0.016339162359343296 + } + ] +} as const; diff --git a/web/src/lib/record.ts b/web/src/lib/record.ts new file mode 100644 index 0000000..20d2336 --- /dev/null +++ b/web/src/lib/record.ts @@ -0,0 +1,47 @@ +// Records a whole FLASH run tile by tile (the row state at each tile_done pulse), for the docs demos. Same observation +// rule as the visualizer: the tile completed on an edge is order[tiles - 1] of that run. +import { quantize } from "@/lib/quantize"; +import { flashTileOrder } from "@/lib/schedule"; +import type { EngineModule } from "@/lib/state-schema"; +import type { TileSample } from "@/lib/softmax-cell"; + +const MAX_CYCLES = 400_000; + +export function recordFlashRun( + mod: EngineModule, + q: number[][], + k: number[][], + v: number[][], + causal: boolean, + qrows: number, + latency: number, +): TileSample[] { + const n = q.length; + const qz = quantize(q, k, v); + const order = flashTileOrder(n, qrows, causal); + const sim = new mod.TinyAttnSim(); + const samples: TileSample[] = []; + const observe = (s: { tileDone: boolean; tiles: number }) => { + if (!s.tileDone) return; + const ref = order[s.tiles - 1]; + if (!ref) return; + const t = sim.getTile(); + samples.push({ qi: ref.qi, kj: ref.kj, z: t.z, m: t.m, l: t.l, alpha: t.alpha }); + }; + try { + sim.configure(n, qrows, causal, 0, false, latency, qz.cfgScale); + sim.loadQKV(qz.q, qz.k, qz.v); + sim.start(); + for (let i = 0; i < MAX_CYCLES; i++) { + const s = sim.step(); + observe(s); + if (s.done) { + observe(sim.step()); + break; + } + } + } finally { + sim.delete(); + } + return samples; +} diff --git a/web/src/pages/docs/architecture.astro b/web/src/pages/docs/architecture.astro new file mode 100644 index 0000000..9f8bade --- /dev/null +++ b/web/src/pages/docs/architecture.astro @@ -0,0 +1,90 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +--- + + +

+ The top level is tiny_attn_top. A host loads Q, K and V into a model of off-chip memory, starts the engine, + and reads back the output. Two interchangeable controllers drive one tile core. One selects the FLASH schedule, and the + other the NAIVE schedule. +

+ + + + host + + hbm_model + words, byte counters + + dma + bursts, latency + + flash_ctrl + tiles, prefetch + + naive_ctrl + four passes + + K/V buffers + double-buffered + + attn_tile + + MM1 S = Q·Kᵀ (4×4 PEs) + + scale, mask, online softmax + + MM2 P·V (4×4 PEs) + O accumulator, normalize + + sram_bank + output staging + + perf_counters + cycles, bytes, tiles + + + + + + + + + + + + +

Memory

+
    +
  • hbm_model is a 2,048-word memory: Q, K and V rows, the output, and the NAIVE score and probability tiles. It counts the bytes read and written.
  • +
  • dma moves one burst at a time, with a configurable first-word latency (0 to 64 cycles) and one word per cycle after it.
  • +
  • K/V buffers are two K tiles and two V tiles, each 4 rows. Without overlap only the first pair is used. With overlap, the second pair is filled while the first is in use.
  • +
  • sram_bank stages output rows before the DMA writes them.
  • +
+ +

Controllers

+

+ flash_ctrl loops over Q blocks and, inside each block, over the K/V tiles. It issues the DMA loads, the + weight loads and the group tiles, skips tiles above the causal diagonal, and normalizes and stores each block. + naive_ctrl runs the four passes of the standard softmax over each row block, writing and reading the + score and probability tiles through HBM. +

+ +

Tile core

+

+ attn_tile processes one 4-row group against one K/V tile. The two 4×4 systolic arrays are the matrix + products. Between them sit the scale-and-mask unit, the row maximum, the exp2 unit and the sum. The O accumulator + rescales the running output by α and adds the new product. Five operations share the datapath: FLASH (online), + and the four NAIVE passes. +

+ +

Timing

+

+ The schedule is exact: busy cycles follow the model in sim/golden/schedule.py, and the end-to-end test + checks them for every configuration it runs. Overlap changes the schedule, so its cycle counts are pinned as + regression baselines in sim/perf_baseline.json. +

+

+ The design has not been synthesized for an FPGA yet (Phase 10). The numbers on this site come from simulation. +

+
diff --git a/web/src/pages/docs/fixed-point.astro b/web/src/pages/docs/fixed-point.astro new file mode 100644 index 0000000..1f70240 --- /dev/null +++ b/web/src/pages/docs/fixed-point.astro @@ -0,0 +1,79 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +import QuantizerDemo from "@/components/docs/QuantizerDemo"; +import { MEASURED } from "@/lib/measured"; + +const presetRows = MEASURED.presetErrors; +--- + + +

+ The engine is integer hardware. Every value has a fixed format, and the formats were chosen by a numerics study before any RTL was written. + The golden model in sim/golden/fixed.py uses the same integers, so the RTL can be checked bit for bit. +

+ +

Formats

+ + + + + + + + + + + + + + + + +
ValueFormatNotes
Q, K, Vint8Symmetric per-tensor scale, set on the host (see below)
S = Q·Kᵀint17Signed dot product of int8 values
cfg_scaleUQ4.20, 24 bitsFolds s_Q · s_K · log₂e / √d into one multiplier
Z, mQ8.8, signedScaled scores in log₂ units, and their running maximum
Z − mQ9.8, signedAlways ≤ 0; the input of exp2
2^(Z − m), αUQ1.15exp2 output and the rescale factor
PUQ1.8, 9 bitsProbabilities, 0 to 256
P·V (one tile)int18, signedMatrix product in the second array
l (running sum)18 bits, unsignedReal value × 2¹²
O (accumulator)26 bits, signedReal value × 2⁴, in V units
ReciprocalUQ1.16 mantissa, 5-bit exponentUsed once per row, at the end
OutputQ8.8, signedDequantize with s_V: value = out / 256 · s_V
+ +

Quantization

+

+ The host picks one scale per tensor: s = max|x| / 127, then int8 = round(x / s) with ties to + even, clipped to ±127. The scales of Q and K go into cfg_scale, so the datapath never multiplies by a + scale. Try it: +

+ + +

The exp2 unit

+

+ 2^(x/256) for x ≤ 0 is split into an integer shift and a fraction. The fraction indexes a + 256-entry table of 16-bit values. The integer part is a right shift. Results that shift past 16 bits are exactly zero, + and the output is truncated, not rounded. The table is generated by sim/golden/gen_luts.py, and CI checks + that the committed table is up to date. +

+

The unit is tested exhaustively over its input range, so there is no sampling gap.

+ +

Measured error

+

+ Errors are against float attention on the same inputs, in float units. The first table is for each preset, with the + engine's output after dequantization. The second is from the recorded 1000-seed study at N = 32, in V units (one int8 + step of V). The study separates the datapath error (dp, on the INT8 inputs) from the total error + (tot, which includes input quantization). +

+ + + + {presetRows.map((r) => ( + + ))} + +
PresetNCausalMax |engine − float|
{r.name}{r.n}{r.causal ? "yes" : "no"}{r.maxErr.toFixed(4)}
+ + + + {Object.entries(MEASURED.study.flashVsNaive).map(([mode, r]) => ( + + ))} + +
Study, N = 32, peaky inputsdp maxdp meantot maxtot mean
{mode === "flash" ? "FLASH" : "NAIVE"}{r.dpMax.toFixed(3)}{r.dpMean.toFixed(4)}{r.totMax.toFixed(3)}{r.totMean.toFixed(4)}
+

+ For the default format at N = 32 (peaky inputs), the datapath error averages 0.07 of a V step for FLASH and 0.10 for + NAIVE. The largest errors are 1.26 steps for FLASH and 1.74 for NAIVE. The total error is larger because it also includes the rounding of + the inputs to INT8. The full study output is recorded in sim/golden/study_results_1000seeds.txt. +

+
diff --git a/web/src/pages/docs/flashattention.astro b/web/src/pages/docs/flashattention.astro new file mode 100644 index 0000000..a10f1ec --- /dev/null +++ b/web/src/pages/docs/flashattention.astro @@ -0,0 +1,36 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +import TrafficDemo from "@/components/docs/TrafficDemo"; +--- + + +

+ FlashAttention computes attention in tiles. A block of query rows stays on chip. The engine streams key and value tiles + past it, one 4-row tile at a time, folds each tile into the running state of + online softmax, and writes the output once at the end. The N × N score + matrix never exists in memory. +

+

The schedule

+
    +
  • Q rows are split into blocks. A block holds cfg_qrows rows (4, 8, 16 or 32 here).
  • +
  • For each block, every key and value tile is loaded once and used by all the row groups in the block.
  • +
  • With the causal mask, tiles above the diagonal are skipped, and the K/V loads stop at the block's last needed tile.
  • +
  • The output of a block is normalized and written once.
  • +
+

The traffic

+

+ Each K or V tile is 16 bytes (4 rows of 4 int8 values), each Q tile is 16 bytes, and each O tile is 32 bytes (4 rows of + 4 int16 values). With nb = N / 4 row groups: +

+
    +
  • FLASH: reads 16·nb bytes of Q, plus 32 bytes per K/V tile load. Writes 32·nb bytes of O. A larger Q block means fewer K/V loads.
  • +
  • NAIVE: writes and reads the score tiles Z (32 bytes each) and the probability tiles P (32 bytes each) through HBM, once per pass.
  • +
+

+ The NAIVE schedule is the standard three-pass softmax: pass 1 writes the scores, pass 2 finds the row maxima, pass 3 + writes the probabilities, and pass 4 multiplies them by V. +

+

Compare the bytes

+

The tables come from the golden traffic model, which the cocotb end-to-end test checks against the RTL byte counters.

+ +
diff --git a/web/src/pages/docs/index.astro b/web/src/pages/docs/index.astro new file mode 100644 index 0000000..ee9916a --- /dev/null +++ b/web/src/pages/docs/index.astro @@ -0,0 +1,15 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +--- + + +

Six pages, each with a live demo of the part it explains.

+
    +
  • What is attention: queries, keys and values, and the score matrix.
  • +
  • Online softmax: how a softmax over a row can be computed a tile at a time.
  • +
  • FlashAttention: the tile schedule, and the memory traffic it saves.
  • +
  • Fixed point: the number formats, the exp2 unit, and the measured error.
  • +
  • Architecture: the blocks of the RTL and how they connect.
  • +
  • Verification: what is tested, how, and the results.
  • +
+
diff --git a/web/src/pages/docs/online-softmax.astro b/web/src/pages/docs/online-softmax.astro new file mode 100644 index 0000000..d783cd1 --- /dev/null +++ b/web/src/pages/docs/online-softmax.astro @@ -0,0 +1,32 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +import OnlineSoftmaxDemo from "@/components/docs/OnlineSoftmaxDemo"; +--- + + +

+ A softmax over a row needs the row's maximum before it can exponentiate. The usual recipe makes three passes over the + row: find the maximum, sum the exponentials of the shifted values, then divide. A tiled engine sees the row a few keys + at a time, so it cannot do that without storing the whole row. +

+

The online softmax keeps two running values for each row: the maximum m so far, and the sum l of exponentials relative to it. When a new tile brings keys with a larger maximum, the old sum was relative to a smaller maximum, so it must shrink. It is multiplied by

+

α = exp(m_old − m_new)

+

+ and the same factor rescales the output accumulator O, which is a sum of exponentials times values. After + the last tile, O / l is the exact attention output for that row. Nothing about the row has to be kept + except m, l and O. +

+

In the engine

+

+ The engine works in log base 2, so it uses 2^z in place of exp. The scores are scaled to + z = s · log₂e / √d and the rescale is α = 2^(m_old − m_new). The power of two is computed by + the exp2 unit (see Fixed point), and α is stored as an unsigned 16-bit + fraction. +

+

Watch one row

+

+ The demo runs the engine on the toy sentence, records the row state after each K/V tile, and shows it next to the float + softmax of the same inputs. The difference is fixed-point rounding only. +

+ +
diff --git a/web/src/pages/docs/verification.astro b/web/src/pages/docs/verification.astro new file mode 100644 index 0000000..b3698d6 --- /dev/null +++ b/web/src/pages/docs/verification.astro @@ -0,0 +1,105 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +import { MEASURED } from "@/lib/measured"; + +const seq = MEASURED.study.sequence; +--- + + +

+ Every claim on this site is checked by a test in the repository. The golden model is the reference: a Python program + with the same integer formats, which the RTL is compared against on every run. +

+ +

What is checked

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerWhat it checksHow it runs
Unit blocksEach arithmetic block against the golden model, including exhaustive sweeps of the exp2 and reciprocal inputs, and the DMA burst timing cycle by cycle.bash sim/run_unit.sh (cocotb with Verilator)
Tile coreOne tile against the golden trace, including causal masking and multi-group blocks.sim/test_attn_tile.py
Engine, end to endOutput bit for bit; HBM read and write bytes; tile count; K/V loads; busy cycles against the closed-form model. Every sequence length 4 to 32, causal and not, all block sizes, both schedules, overlap on and off, and random HBM latency. Random seeds on top.SEEDS=200 bash sim/run_e2e.sh
Golden modelQuantization, exp2 and reciprocal, both schedules against float attention, traffic formulas, presets, and that the committed LUT table matches its generator.pytest sim/golden
LintVerilator with all warnings on, for every module and the full design.bash sim/lint.sh
WebAssemblyThe compiled module, on golden fixtures for each schedule: output, bytes, tiles, cycles, the last tile's state, probabilities, and the K/V buffer in use.node wasm/test.mjs
Web appQuantization and tile order against the shared fixtures, the visualizer and each docs page in a real browser, at phone width and in both colour schemes.pnpm test, pnpm test:e2e
+

All of these run in GitHub Actions on every pull request. The workflows are rtl.yml, wasm.yml and web.yml.

+ +

Output error

+

Maximum error of the engine's output against float attention, per preset, in float units. These numbers come from the golden model, which the RTL matches bit for bit.

+ + + + {MEASURED.presetErrors.map((r) => ( + + ))} + +
PresetNCausalMax error
{r.name}{r.n}{r.causal ? "yes" : "no"}{r.maxErr.toFixed(4)}
+ +

Accuracy study

+

+ The recorded study (sim/golden/study_results_1000seeds.txt), in V units (one int8 step of V). dp is the datapath + error on the INT8 inputs. tot is the error against float attention on the original inputs. +

+ + + + {seq.map((r) => ( + + + + + + ))} + +
NCausaldp maxdp meantot maxtot mean
{r.n}{r.causal ? "yes" : "no"}{r.dpMax.toFixed(3)}{r.dpMean.toFixed(4)}{r.totMax.toFixed(3)}{r.totMean.toFixed(4)}
+ +

Cycles at N = 32

+

+ Busy cycles at N = 32, non-causal, with 10 cycles of HBM latency. The sequential FLASH rows match the closed-form + schedule exactly. The overlap and NAIVE rows are pinned baselines from the RTL, and the test fails if they change. +

+ + + + {MEASURED.perfN32.map((r) => ( + + ))} + +
ConfigurationBusy cycles
{r.label}{r.cycles}
+ +

Not verified yet

+
    +
  • Synthesis for an FPGA. The timing and area numbers are not measured yet (Phase 10).
  • +
  • Real hardware. Everything here runs in simulation or in WebAssembly.
  • +
+
diff --git a/web/src/pages/docs/what-is-attention.astro b/web/src/pages/docs/what-is-attention.astro new file mode 100644 index 0000000..f492d75 --- /dev/null +++ b/web/src/pages/docs/what-is-attention.astro @@ -0,0 +1,33 @@ +--- +import DocsLayout from "@/components/docs/DocsLayout.astro"; +import AttentionDemo from "@/components/docs/AttentionDemo"; +--- + + +

+ Attention lets each token in a sequence take information from the others. For token i it builds a + query vector q_i. Every token j offers a key k_j and a value v_j. + The score of j for i is the dot product q_i · k_j, scaled by + 1/√d. A softmax turns each row of scores into weights that sum to one. The output for token + i is the weighted sum of the values. +

+

In matrix form, with N tokens of dimension d:

+

O = softmax(Q Kᵀ / √d) V

+

+ The middle term, S = Q Kᵀ, is an N × N matrix. For a 2,048-token context that is about four + million scores for every head. Storing it is what makes naive attention expensive in memory, and it is the reason + FlashAttention exists. +

+

Causal masks

+

+ In a language model, token i must not see later tokens. A causal mask sets the scores for j > i + to minus infinity before the softmax, so their weights are zero. The engine has a causal mode; with it, half of the + score matrix is never computed. +

+

Try it

+

+ The demo uses the float reference, not the engine. Pick a preset and a query token. In the toy sentence, the pronoun "it" gives + "cat" about 0.66 of its weight and "mouse" about 0.15. +

+ +
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index bba47d0..c32f390 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -1,16 +1,52 @@ --- import Layout from "@/layouts/Layout.astro"; +import Hero from "@/components/docs/Hero"; --- - -
-

TinyAttention

-

- A real, synthesizable FlashAttention engine in SystemVerilog, compiled to WebAssembly and - running live in your browser. Under construction. -

-

- Toolchain spike → -

+ +
+
+

TinyAttention

+

FlashAttention, built in hardware and run in your browser.

+

+ A synthesizable SystemVerilog engine for attention. It computes the output one tile at a time, so the N×N score + matrix never goes to memory. The RTL is verified bit-exact against a golden model, and the same RTL runs here as + WebAssembly. +

+ +
+ +
+

The schedule

+ +
+ +
+
+
Bit-exact
+

Output, HBM traffic, tile counts and cycle counts match the golden model on every run in the test sweep.

+
+
+
No N×N matrix
+

At N = 32, FLASH moves 1536 bytes against 7296 for the naive schedule, with the same result.

+
+
+
In the browser
+

The Verilator model is compiled to an 83 KB (gzip) WebAssembly module. You step it clock by clock.

+
+
+ +
+

+ Start with what attention computes, + then the online softmax that makes the tiling possible. +

+
diff --git a/web/src/pages/og.astro b/web/src/pages/og.astro new file mode 100644 index 0000000..97b8ac3 --- /dev/null +++ b/web/src/pages/og.astro @@ -0,0 +1,53 @@ +--- +// The social card, 1200 x 630. Captured to public/og.png by web/scripts/capture-assets.mjs; not linked from the site. +import { flashTileOrder } from "@/lib/schedule"; + +const N = 16; +const QROWS = 8; +const order = flashTileOrder(N, QROWS, true); +const blocks = N / 4; +const done = new Set(order.map((t) => `${t.qi},${t.kj}`)); +--- + + + + + + + TinyAttention + + + +
+
+
TinyAttention
+

FlashAttention in SystemVerilog

+

Bit-exact against a golden model. Compiled to WebAssembly. Run it in your browser.

+
+ +
+ + diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 8a26f44..ad36c9f 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -9,6 +9,7 @@ --ok: #3fb950; --err: #f85149; --panel: #161b22; + --line: #30363d; } :root[data-theme="light"] { @@ -19,6 +20,7 @@ --ok: #1a7f37; --err: #cf222e; --panel: #f6f8fa; + --line: #d0d7de; } body { From 946de141b6e9d9600c086ebfb240d2b84e6f6f6b Mon Sep 17 00:00:00 2001 From: Dineth Hettiarachchi Date: Mon, 26 Oct 2026 12:04:13 +0530 Subject: [PATCH 2/2] fix: round preset errors in measured.ts so the freshness test is stable across BLAS builds CI's golden test failed on the last digits of maxErr (float64 summation order differs between machines). The docs show four decimals; rounding to six makes the generated file platform-independent. --- sim/golden/export_web_data.py | 3 ++- web/src/lib/measured.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sim/golden/export_web_data.py b/sim/golden/export_web_data.py index c83c42e..fdf882a 100644 --- a/sim/golden/export_web_data.py +++ b/sim/golden/export_web_data.py @@ -151,7 +151,8 @@ def preset_errors(): res = fixed.flash(qz, n, p["causal"], qblock=min(4, n)) out_f = fixed.dequant_out(res, qz) ref, _ = attention(q, k, v, p["causal"]) - out.append(dict(name=p["name"], n=n, causal=bool(p["causal"]), maxErr=float(np.max(np.abs(out_f - ref))))) + # six decimals: the last bits of a float64 sum depend on the BLAS build, and the freshness test must not + out.append(dict(name=p["name"], n=n, causal=bool(p["causal"]), maxErr=round(float(np.max(np.abs(out_f - ref))), 6))) return out diff --git a/web/src/lib/measured.ts b/web/src/lib/measured.ts index b519cc6..74557f5 100644 --- a/web/src/lib/measured.ts +++ b/web/src/lib/measured.ts @@ -111,37 +111,37 @@ export const MEASURED = { "name": "toy_sentence", "n": 12, "causal": true, - "maxErr": 0.004894227790511407 + "maxErr": 0.004894 }, { "name": "previous_token", "n": 16, "causal": true, - "maxErr": 0.010518908053282039 + "maxErr": 0.010519 }, { "name": "attention_sink", "n": 16, "causal": true, - "maxErr": 0.02534997660881233 + "maxErr": 0.02535 }, { "name": "uniform", "n": 16, "causal": false, - "maxErr": 0.002772959153543364 + "maxErr": 0.002773 }, { "name": "self", "n": 16, "causal": false, - "maxErr": 0.02599748051857198 + "maxErr": 0.025997 }, { "name": "random", "n": 16, "causal": false, - "maxErr": 0.016339162359343296 + "maxErr": 0.016339 } ] } as const;