wolfpine 2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wolfpine-2.1/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Nick Brown
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
wolfpine-2.1/PKG-INFO ADDED
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: wolfpine
3
+ Version: 2.1
4
+ Summary: A framework for simulating AI accelerators, with Tenstorrent Wormhole and Blackhole backends
5
+ Author: Nick Brown
6
+ License-Expression: BSD-3-Clause
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pyyaml
13
+ Requires-Dist: pynng
14
+ Requires-Dist: flatbuffers
15
+ Requires-Dist: pyarrow
16
+ Requires-Dist: pyelftools
17
+ Provides-Extra: dev
18
+ Requires-Dist: ruff; extra == "dev"
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # Wolfpine
23
+
24
+ **A framework for simulating AI accelerators, in pure Python.**
25
+
26
+ Wolfpine is a hackable software framework for building cycle-approximate
27
+ simulators of AI accelerator hardware. The core — the memory map, the
28
+ interconnect, the RISC-V cores, the clock and reset fabric, the cost and
29
+ trace machinery — is architecture-agnostic; a target is described by an
30
+ *architecture profile* plus a thin device class, not by a fork.
31
+
32
+ The framework ships with backends for the Tenstorrent architecture,
33
+ **Wormhole** and **Blackhole**, built from the public
34
+ [Tenstorrent ISA Documentation](https://github.com/tenstorrent/tt-isa-documentation/tree/main).
35
+ Both run real tt-metal programs unmodified, on the full default worker grid —
36
+ 72 workers on Wormhole, 130 on Blackhole. Note that it is not cycle accurate
37
+ (or intended to be). The idea was to build a simulator from public
38
+ documentation, so developers can experiment with writing accelerator code
39
+ without needing physical hardware — and, because it is Python all the way
40
+ down, poke at simulator state directly from a driver script.
41
+
42
+ ## Contents
43
+
44
+ - [Installation](#installation)
45
+ - [Using Wolfpine as a simulator for tt-metal](#using-wolfpine-as-a-simulator-for-tt-metal)
46
+ - [Repository overview](#repository-overview)
47
+ - [What the timing model claims, and what it does not](#what-the-timing-model-claims-and-what-it-does-not)
48
+ - [Key parts of the simulator](#key-parts-of-the-simulator)
49
+
50
+ Profiling a kernel: [`driver/wormhole/docs/profiling.md`](driver/wormhole/docs/profiling.md)
51
+ walks through every output the simulator can produce.
52
+ Building tooling on those outputs: [`docs/trace-schema.md`](docs/trace-schema.md)
53
+ is the stable, versioned schema — field meanings, units and what you may
54
+ rely on.
55
+
56
+ ## Installation
57
+
58
+ Wolfpine is pure Python and requires Python ≥ 3.10. Clone the repository and install it in
59
+ editable mode, which pulls in its dependencies (numpy, pyyaml, pynng, flatbuffers,
60
+ pyarrow, pyelftools):
61
+
62
+ ```bash
63
+ git clone https://github.com/hedgehope/wolfpine.git
64
+ cd wolfpine
65
+ pip install -e . # add [dev] for the ruff + pytest tooling: pip install -e .[dev]
66
+ ```
67
+
68
+ This installs the `framework` simulator library. The example drivers live under `driver/`
69
+ and are run from the repository root, so keep the repo root on your `PYTHONPATH` when
70
+ invoking them directly (the tt-metal flow below wires this up for you automatically):
71
+
72
+ ```bash
73
+ export PYTHONPATH=$PWD:$PYTHONPATH
74
+ ```
75
+
76
+ ## Using Wolfpine as a simulator for tt-metal
77
+
78
+ tt-metal's UMD has a "simulation" chip backend: point `TT_METAL_SIMULATOR` at the
79
+ [`driver/wormhole`](driver/wormhole) directory and UMD launches Wolfpine in place of real
80
+ silicon, so an ordinary tt-metal program runs against the simulator over a socket — no
81
+ code changes, exactly as it would run on hardware. The minimal setup:
82
+
83
+ ```bash
84
+ export TT_METAL_RUNTIME_ROOT=/path/to/tt-metal # your built tt-metal checkout
85
+ export TT_METAL_SIMULATOR="$PWD/driver/wormhole" # the switch: route execution to Wolfpine
86
+ export TT_METAL_SLOW_DISPATCH_MODE=1 # the only launch path the sim models
87
+ export LD_LIBRARY_PATH="$TT_METAL_RUNTIME_ROOT/build/lib:$LD_LIBRARY_PATH"
88
+
89
+ # then run any tt-metal program, e.g. an upstream example:
90
+ cd "$TT_METAL_RUNTIME_ROOT/build/programming_examples"
91
+ ./metal_example_add_2_integers_in_compute
92
+ ```
93
+
94
+ A set of ready-to-run example programs, plus a build-and-run test suite, lives under
95
+ [`examples`](examples). For the full walkthrough — building the
96
+ examples, the `TT_SIM_TENSIX_COORDS` grid-pinning knob, diagnostics, structured tracing and
97
+ the deadlock watchdog — see **[`driver/wormhole/README.md`](driver/wormhole/README.md)**.
98
+
99
+ ## Repository overview
100
+
101
+ The simulator implementation is in the [framework](https://github.com/hedgehope/wolfpine/tree/main/framework) directory, with the [driver](https://github.com/hedgehope/wolfpine/tree/main/driver) directory providing a range of examples that illustrate running the simulator. These are individually documented, but to summarise:
102
+
103
+ * [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) is where you likely want to go to. It provides an implementation of a Wormhole, currently with one DRAM tile and one Tensix block (although this is easy to expand, although will likely be slow!) It holds a set of example tt-metal programs that are built against a local tt-metal checkout and run against the simulator — exactly as they would run on hardware, only the device is the simulator. Each validates its own results, so the examples double as a test suite. See its README for how to build and run them.
104
+ * [blackhole](https://github.com/hedgehope/wolfpine/tree/main/driver/blackhole) is the Blackhole driver: the same wire bridge and machinery as wormhole (all shared in [`framework/bridge`](https://github.com/hedgehope/wolfpine/tree/main/framework/bridge)), pointed at a Blackhole device instead. Point `TT_METAL_SIMULATOR` at `driver/blackhole` to route execution there. Workers are materialised on demand by the wire bridge, so a program gets exactly the grid it launches on, up to the full 130; see its README.
105
+ * [simple](https://github.com/hedgehope/wolfpine/tree/main/driver/simple) are very basic examples, demonstrating the memory subsystem and running codes on a vanilla RV32IM CPU.
106
+
107
+ ### What the timing model claims, and what it does not
108
+
109
+ The simulator carries a cost model of **documented provenance**: every cycle
110
+ figure is traceable to the ISA documentation or a vendor source, there are
111
+ **zero un-sourced estimates** (asserted by test), and it has been checked
112
+ against real silicon. Stated precisely:
113
+
114
+ > Runs real tt-metal programs unmodified on both architectures, on the full
115
+ > default grid, with a timing model of documented provenance, corroborated
116
+ > against silicon at the **slope** and **launch** level on both parts, and
117
+ > **by mechanism** for NoC-bound work on both parts.
118
+
119
+ Four caveats belong with that sentence rather than under it, because three of
120
+ them are permanent:
121
+
122
+ - **It is a floor.** Every bound is charged at its low end, by rule, so it
123
+ under-predicts wherever hardware sits above a documented minimum.
124
+ - **Mechanism-level checking is one leg of three.** The RV-bound leg is
125
+ Blackhole-only (Wormhole documents no CSRs, so there is no retired-instruction
126
+ count) and currently fails its bar; the Tensix mechanism leg cannot be built
127
+ at all from the counters the hardware exposes.
128
+ - **That failure has a single, sourced cause** — integer divide, where the
129
+ documentation gives only a 6–33 cycle range and no algorithm, so a
130
+ magnitude-dependent term cannot be sourced at any provenance.
131
+ - **Energy is ranking-level only**, and barely beats a model that knows nothing
132
+ but the cycle count. Absolute joules are out of reach of the instrument.
133
+
134
+ The full evidence, including what was proven *impossible* rather than merely
135
+ left undone, is in [`ROADMAP.md`](ROADMAP.md).
136
+
137
+ Optional diagnostic information can be provided by each RISC-V baby core, the NoC, the Tensix co-processor and memory, enabling tracing of the execution of a program. This is currently at the instruction and architectural state level, but could be enhanced in the future to provide feedback to developers around potential code bottlenecks or other issues.
138
+
139
+ This is written in Python, mainly to make it easy for people to hackaround and experiment with things. If you want to add some functionality, or fix a bug, then please feel free to go ahead and raise a PR. This is still work in progress, so also please raise issues etc as you find them!
140
+
141
+ ## Key parts of the simulator
142
+
143
+ There are a few of key components which are worth highlighting:
144
+
145
+ * [tt_device.Wormhole](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L111) creates the Wormhole, currently with a single DRAM tile and a tensix tile. Here you can see the NoC coordinates specified of each, and also the booleans provided to _TensixTile_ are whether to report diagnostic information (the first five for each RISC-V baby core, then next two for each NoC and then the separate Tensix co-processor choices). In the [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) tt-metal flow these are set from the `TT_SIM_DIAG_*` environment variables (see that directory's README).
146
+ * [tt_device.TensixTile](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L186) plumbs everything together within a Tensix tile, setting all the memory addresses and ranges for each individual component. These are all based on the ISA documentation [memory map](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/README.md).
147
+ * [tt_noc](https://github.com/hedgehope/wolfpine/blob/main/framework/network/tt_noc.py) is the implementation of the NoC, it is not yet complete with all the functionality but is sufficient to communicate between tiles and, for example, read and write between DRAM and the Tensix tile.
148
+ * [rv](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/rv) provides the RV32IM implementation with [.ttinsn extension](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/PushTensixInstruction.md#ttinsn-instruction-set-extension). This is fairly self explanatory, providing a pluggable approach to combining ISAs. The [BabyRISCV](https://github.com/hedgehope/wolfpine/blob/main/framework/pe/rv/babyriscv.py) ties this together for the baby RISC-V cores in the Tensix tile, for instance determining the initial PC value after a soft reset as per [here](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/SoftReset.md).
149
+ * [tensix](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/tensix) is the implementation of the Tensix coprocessor as per [here](https://github.com/tenstorrent/tt-isa-documentation/tree/main/WormholeB0/TensixTile/TensixCoprocessor). This is not fully complete, but enough to run a range of codes that use the matrix, vector and scalar units (and all the associated unit implementation required to enable this).
wolfpine-2.1/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Wolfpine
2
+
3
+ **A framework for simulating AI accelerators, in pure Python.**
4
+
5
+ Wolfpine is a hackable software framework for building cycle-approximate
6
+ simulators of AI accelerator hardware. The core — the memory map, the
7
+ interconnect, the RISC-V cores, the clock and reset fabric, the cost and
8
+ trace machinery — is architecture-agnostic; a target is described by an
9
+ *architecture profile* plus a thin device class, not by a fork.
10
+
11
+ The framework ships with backends for the Tenstorrent architecture,
12
+ **Wormhole** and **Blackhole**, built from the public
13
+ [Tenstorrent ISA Documentation](https://github.com/tenstorrent/tt-isa-documentation/tree/main).
14
+ Both run real tt-metal programs unmodified, on the full default worker grid —
15
+ 72 workers on Wormhole, 130 on Blackhole. Note that it is not cycle accurate
16
+ (or intended to be). The idea was to build a simulator from public
17
+ documentation, so developers can experiment with writing accelerator code
18
+ without needing physical hardware — and, because it is Python all the way
19
+ down, poke at simulator state directly from a driver script.
20
+
21
+ ## Contents
22
+
23
+ - [Installation](#installation)
24
+ - [Using Wolfpine as a simulator for tt-metal](#using-wolfpine-as-a-simulator-for-tt-metal)
25
+ - [Repository overview](#repository-overview)
26
+ - [What the timing model claims, and what it does not](#what-the-timing-model-claims-and-what-it-does-not)
27
+ - [Key parts of the simulator](#key-parts-of-the-simulator)
28
+
29
+ Profiling a kernel: [`driver/wormhole/docs/profiling.md`](driver/wormhole/docs/profiling.md)
30
+ walks through every output the simulator can produce.
31
+ Building tooling on those outputs: [`docs/trace-schema.md`](docs/trace-schema.md)
32
+ is the stable, versioned schema — field meanings, units and what you may
33
+ rely on.
34
+
35
+ ## Installation
36
+
37
+ Wolfpine is pure Python and requires Python ≥ 3.10. Clone the repository and install it in
38
+ editable mode, which pulls in its dependencies (numpy, pyyaml, pynng, flatbuffers,
39
+ pyarrow, pyelftools):
40
+
41
+ ```bash
42
+ git clone https://github.com/hedgehope/wolfpine.git
43
+ cd wolfpine
44
+ pip install -e . # add [dev] for the ruff + pytest tooling: pip install -e .[dev]
45
+ ```
46
+
47
+ This installs the `framework` simulator library. The example drivers live under `driver/`
48
+ and are run from the repository root, so keep the repo root on your `PYTHONPATH` when
49
+ invoking them directly (the tt-metal flow below wires this up for you automatically):
50
+
51
+ ```bash
52
+ export PYTHONPATH=$PWD:$PYTHONPATH
53
+ ```
54
+
55
+ ## Using Wolfpine as a simulator for tt-metal
56
+
57
+ tt-metal's UMD has a "simulation" chip backend: point `TT_METAL_SIMULATOR` at the
58
+ [`driver/wormhole`](driver/wormhole) directory and UMD launches Wolfpine in place of real
59
+ silicon, so an ordinary tt-metal program runs against the simulator over a socket — no
60
+ code changes, exactly as it would run on hardware. The minimal setup:
61
+
62
+ ```bash
63
+ export TT_METAL_RUNTIME_ROOT=/path/to/tt-metal # your built tt-metal checkout
64
+ export TT_METAL_SIMULATOR="$PWD/driver/wormhole" # the switch: route execution to Wolfpine
65
+ export TT_METAL_SLOW_DISPATCH_MODE=1 # the only launch path the sim models
66
+ export LD_LIBRARY_PATH="$TT_METAL_RUNTIME_ROOT/build/lib:$LD_LIBRARY_PATH"
67
+
68
+ # then run any tt-metal program, e.g. an upstream example:
69
+ cd "$TT_METAL_RUNTIME_ROOT/build/programming_examples"
70
+ ./metal_example_add_2_integers_in_compute
71
+ ```
72
+
73
+ A set of ready-to-run example programs, plus a build-and-run test suite, lives under
74
+ [`examples`](examples). For the full walkthrough — building the
75
+ examples, the `TT_SIM_TENSIX_COORDS` grid-pinning knob, diagnostics, structured tracing and
76
+ the deadlock watchdog — see **[`driver/wormhole/README.md`](driver/wormhole/README.md)**.
77
+
78
+ ## Repository overview
79
+
80
+ The simulator implementation is in the [framework](https://github.com/hedgehope/wolfpine/tree/main/framework) directory, with the [driver](https://github.com/hedgehope/wolfpine/tree/main/driver) directory providing a range of examples that illustrate running the simulator. These are individually documented, but to summarise:
81
+
82
+ * [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) is where you likely want to go to. It provides an implementation of a Wormhole, currently with one DRAM tile and one Tensix block (although this is easy to expand, although will likely be slow!) It holds a set of example tt-metal programs that are built against a local tt-metal checkout and run against the simulator — exactly as they would run on hardware, only the device is the simulator. Each validates its own results, so the examples double as a test suite. See its README for how to build and run them.
83
+ * [blackhole](https://github.com/hedgehope/wolfpine/tree/main/driver/blackhole) is the Blackhole driver: the same wire bridge and machinery as wormhole (all shared in [`framework/bridge`](https://github.com/hedgehope/wolfpine/tree/main/framework/bridge)), pointed at a Blackhole device instead. Point `TT_METAL_SIMULATOR` at `driver/blackhole` to route execution there. Workers are materialised on demand by the wire bridge, so a program gets exactly the grid it launches on, up to the full 130; see its README.
84
+ * [simple](https://github.com/hedgehope/wolfpine/tree/main/driver/simple) are very basic examples, demonstrating the memory subsystem and running codes on a vanilla RV32IM CPU.
85
+
86
+ ### What the timing model claims, and what it does not
87
+
88
+ The simulator carries a cost model of **documented provenance**: every cycle
89
+ figure is traceable to the ISA documentation or a vendor source, there are
90
+ **zero un-sourced estimates** (asserted by test), and it has been checked
91
+ against real silicon. Stated precisely:
92
+
93
+ > Runs real tt-metal programs unmodified on both architectures, on the full
94
+ > default grid, with a timing model of documented provenance, corroborated
95
+ > against silicon at the **slope** and **launch** level on both parts, and
96
+ > **by mechanism** for NoC-bound work on both parts.
97
+
98
+ Four caveats belong with that sentence rather than under it, because three of
99
+ them are permanent:
100
+
101
+ - **It is a floor.** Every bound is charged at its low end, by rule, so it
102
+ under-predicts wherever hardware sits above a documented minimum.
103
+ - **Mechanism-level checking is one leg of three.** The RV-bound leg is
104
+ Blackhole-only (Wormhole documents no CSRs, so there is no retired-instruction
105
+ count) and currently fails its bar; the Tensix mechanism leg cannot be built
106
+ at all from the counters the hardware exposes.
107
+ - **That failure has a single, sourced cause** — integer divide, where the
108
+ documentation gives only a 6–33 cycle range and no algorithm, so a
109
+ magnitude-dependent term cannot be sourced at any provenance.
110
+ - **Energy is ranking-level only**, and barely beats a model that knows nothing
111
+ but the cycle count. Absolute joules are out of reach of the instrument.
112
+
113
+ The full evidence, including what was proven *impossible* rather than merely
114
+ left undone, is in [`ROADMAP.md`](ROADMAP.md).
115
+
116
+ Optional diagnostic information can be provided by each RISC-V baby core, the NoC, the Tensix co-processor and memory, enabling tracing of the execution of a program. This is currently at the instruction and architectural state level, but could be enhanced in the future to provide feedback to developers around potential code bottlenecks or other issues.
117
+
118
+ This is written in Python, mainly to make it easy for people to hackaround and experiment with things. If you want to add some functionality, or fix a bug, then please feel free to go ahead and raise a PR. This is still work in progress, so also please raise issues etc as you find them!
119
+
120
+ ## Key parts of the simulator
121
+
122
+ There are a few of key components which are worth highlighting:
123
+
124
+ * [tt_device.Wormhole](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L111) creates the Wormhole, currently with a single DRAM tile and a tensix tile. Here you can see the NoC coordinates specified of each, and also the booleans provided to _TensixTile_ are whether to report diagnostic information (the first five for each RISC-V baby core, then next two for each NoC and then the separate Tensix co-processor choices). In the [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) tt-metal flow these are set from the `TT_SIM_DIAG_*` environment variables (see that directory's README).
125
+ * [tt_device.TensixTile](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L186) plumbs everything together within a Tensix tile, setting all the memory addresses and ranges for each individual component. These are all based on the ISA documentation [memory map](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/README.md).
126
+ * [tt_noc](https://github.com/hedgehope/wolfpine/blob/main/framework/network/tt_noc.py) is the implementation of the NoC, it is not yet complete with all the functionality but is sufficient to communicate between tiles and, for example, read and write between DRAM and the Tensix tile.
127
+ * [rv](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/rv) provides the RV32IM implementation with [.ttinsn extension](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/PushTensixInstruction.md#ttinsn-instruction-set-extension). This is fairly self explanatory, providing a pluggable approach to combining ISAs. The [BabyRISCV](https://github.com/hedgehope/wolfpine/blob/main/framework/pe/rv/babyriscv.py) ties this together for the baby RISC-V cores in the Tensix tile, for instance determining the initial PC value after a soft reset as per [here](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/SoftReset.md).
128
+ * [tensix](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/tensix) is the implementation of the Tensix coprocessor as per [here](https://github.com/tenstorrent/tt-isa-documentation/tree/main/WormholeB0/TensixTile/TensixCoprocessor). This is not fully complete, but enough to run a range of codes that use the matrix, vector and scalar units (and all the associated unit implementation required to enable this).
@@ -0,0 +1,10 @@
1
+ """Wolfpine — a framework for simulating AI accelerators.
2
+
3
+ The core here is architecture-agnostic: the memory map, the interconnect, the
4
+ RISC-V cores, the clock and reset fabric, and the cost and trace machinery are
5
+ all parameterised by an :mod:`framework.arch` profile. A target is a profile
6
+ plus a thin device subclass, not a fork.
7
+
8
+ The backends that ship today are the Tenstorrent architectures, Wormhole and
9
+ Blackhole, in :mod:`framework.device`.
10
+ """
@@ -0,0 +1,387 @@
1
+ """What *this* Wolfpine gets right — an assertion target for external suites.
2
+
3
+ Why this is not ``__version__``
4
+ -------------------------------
5
+ The compiler team ran their multicast suite against a Wolfpine that delivered
6
+ correctly-encoded NoC 1 multicasts to nobody and reported success. Every run was
7
+ green because nothing was delivered, not because anything worked, and there was
8
+ no way to tell from the outside. Their question was "is there a version marker
9
+ we can assert on?", and the honest answer is that a version number is the wrong
10
+ shape for it: ``0.7.3`` tells you which build you have, never whether that build
11
+ has the fix, and a consumer who pins ``>= 0.7.3`` has encoded a fact about our
12
+ release history into their test suite rather than a fact about what they need.
13
+
14
+ So what is published here is **behaviour**: named guarantees an outside suite
15
+ may depend on. A name means the same thing forever; it is either present or it
16
+ is not; and asserting on it says what the suite actually depends on.
17
+
18
+ Most entries are of one shape — Wolfpine used to get something silently wrong and
19
+ now refuses it — and for a long time that was the whole registry. It is
20
+ deliberately not the definition. ``dram-interleaved-bank-distinctness`` is a
21
+ **modelling property**: nothing raises, no environment variable switches it off,
22
+ and there is no exception class behind it. It is here because the test it
23
+ replaces was one an outside consumer was writing *by hand* — deriving, from our
24
+ internals and our shipped SoC descriptors, that DRAM banks are separate stores
25
+ at separate coordinates — in order to trust their own gate. That derivation is
26
+ precisely what this registry exists to absorb: the question a consumer needs
27
+ answered is "can a run against this build see the thing I am testing?", and a
28
+ substrate that is missing makes a suite vacuous just as surely as a guard that
29
+ does not fire. So the bar for an entry is that a consumer would otherwise have
30
+ to establish it themselves, not that something throws.
31
+
32
+ The difference does change how such an entry is *used*, and the guarantee text
33
+ says so: a property marker tells you the substrate is there, it does not tell
34
+ you when you have broken your own kernel.
35
+
36
+ Asserting on it
37
+ ---------------
38
+ From Python, in a suite's session setup::
39
+
40
+ from framework.behaviour import require
41
+
42
+ require("noc1-multicast-corner-order")
43
+
44
+ which raises :class:`UnsupportedBehaviour` — naming what is missing, what this
45
+ build does have, and where to read about it — rather than letting the suite
46
+ collect another set of vacuous green results. Against a Wolfpine older than this
47
+ module the ``import`` fails instead, which is the same outcome at the same
48
+ moment; a consumer should not wrap it in ``except ImportError``, because a
49
+ checkout without this module is precisely the one whose results are suspect.
50
+
51
+ From a shell, a Makefile, or a C++ harness's test runner::
52
+
53
+ python3 -m framework.behaviour --require noc1-multicast-corner-order
54
+
55
+ exits 0 when every named behaviour is present and 1 when any is missing, with
56
+ the missing names on stderr. ``python3 -m framework.behaviour`` with no arguments
57
+ lists what this build guarantees, one name per line, so a consumer can record
58
+ the set a run was made against.
59
+
60
+ :func:`supports` is the non-raising form, for a suite that would rather skip
61
+ than fail.
62
+
63
+ This module imports nothing from the rest of Wolfpine and nothing outside the
64
+ standard library, so asserting on it costs no simulator construction and cannot
65
+ be broken by an import cycle.
66
+
67
+ Adding one, and why forgetting is caught
68
+ ----------------------------------------
69
+ A registry nobody is forced to update decays into a lie, and this one is worth
70
+ more than the average: its whole purpose is to be trusted by someone who cannot
71
+ see our tree. Two guards hold it to the code.
72
+
73
+ * **Forward.** Every entry names the test that pins its guarantee, and
74
+ ``behaviour_test.py`` imports that module and looks the function up. An entry
75
+ whose test is renamed or deleted fails immediately, so no guarantee can
76
+ outlive the thing that checks it.
77
+ * **Backward.** The registry is *not* the only place a new guard has to be
78
+ mentioned, because that is the mention people forget. ``behaviour_test.py``
79
+ parses every non-test module under ``framework/`` for exception classes and
80
+ requires each one to be either registered here or listed in
81
+ ``_NOT_A_GUARANTEE`` with a reason. Adding a loudness guard to the simulator
82
+ therefore turns the suite red until somebody has decided, in writing, whether
83
+ an outside consumer would want to assert on it. That decision is the point;
84
+ the registry is just where the "yes" answers land.
85
+
86
+ The dates are the day the guarantee first held in ``main``, recorded because it
87
+ is occasionally useful for bisection. They are not a version and nothing should
88
+ compare them.
89
+ """
90
+
91
+ from __future__ import annotations
92
+
93
+ import argparse
94
+ import sys
95
+ from types import MappingProxyType
96
+ from typing import NamedTuple
97
+
98
+ #: Where the prose lives, quoted in the failure message so a consumer who hits
99
+ #: it has somewhere to go.
100
+ DOCS = "docs/running-tt-metal-on-the-simulator.md, section 7"
101
+
102
+
103
+ class Behaviour(NamedTuple):
104
+ """One named, externally assertable guarantee about this simulator."""
105
+
106
+ #: Stable identifier. Never renamed and never reused for another meaning —
107
+ #: a consumer's test file outlives our refactors.
108
+ name: str
109
+ #: What a run against this build will do, in terms a consumer can observe
110
+ #: from outside: what raises, what is delivered, what is counted.
111
+ guarantee: str
112
+ #: ``YYYY-MM-DD`` the guarantee first held in ``main``. Informational.
113
+ since: str
114
+ #: ``module:function`` of the test that pins it. Checked to exist.
115
+ pinned_by: str
116
+
117
+
118
+ _BEHAVIOURS = (
119
+ Behaviour(
120
+ name="noc1-multicast-corner-order",
121
+ guarantee=(
122
+ "A NoC multicast whose rectangle corners are ordered for the wrong "
123
+ "NoC raises framework.network.multicast_order.MulticastOrderError at "
124
+ "the point of issue. Before this, both wrong orderings completed "
125
+ "green: low-corner-first on a translated NoC 1 was delivered to "
126
+ "every destination (silicon delivers it to none and the sender's "
127
+ "write barrier never retires), and the reverse enumerated an empty "
128
+ "rectangle, so the barrier retired having sent nothing."
129
+ ),
130
+ since="2026-08-19",
131
+ pinned_by=(
132
+ "framework.network.multicast_order_test:"
133
+ "test_a_translated_noc1_multicast_written_low_first_raises"
134
+ ),
135
+ ),
136
+ Behaviour(
137
+ name="riscv-ebreak-halts",
138
+ guarantee=(
139
+ "A baby RISC-V executing ebreak raises "
140
+ "framework.pe.rv.breakpoint.RiscvBreakpoint naming the core and PC. "
141
+ "ebreak is what a kernel ASSERT(), an LLK_ASSERT under "
142
+ "TT_METAL_LLK_ASSERTS=1, and __builtin_trap() all lower to, and "
143
+ "Wolfpine used to decode it as a no-op — so a program whose own "
144
+ "authors had declared a state impossible ran past it and reported "
145
+ "success while the same binary stopped dead on silicon. Set "
146
+ "TT_SIM_IGNORE_EBREAK=1 to restore the old skip-and-continue."
147
+ ),
148
+ since="2026-08-04",
149
+ pinned_by="framework.pe.rv.breakpoint_test:test_ebreak_names_the_core_and_pc",
150
+ ),
151
+ Behaviour(
152
+ name="dram-interleaved-bank-distinctness",
153
+ guarantee=(
154
+ "Every DRAM bank tt-metal can interleave a buffer over is separate "
155
+ "storage reachable at its own NoC 0 coordinate: 12 banks on "
156
+ "Wormhole (6 channels x two 1 GiB dram_views), 8 on Blackhole (8 "
157
+ "channels x one view), counted from the shipped "
158
+ "driver/<arch>/soc_descriptor.yaml. A write to one bank is "
159
+ "invisible in every other; each bank's descriptor coordinate is a "
160
+ "distinct cell that resolves to its own channel's tile (including "
161
+ "the second worker-visible endpoint of a Wormhole channel); and "
162
+ "each channel is modelled whole, so every view's range is backed. "
163
+ "A host scatter and a kernel gather that disagree about page size, "
164
+ "bank count or a bank's coordinate therefore corrupt data, as they "
165
+ "do on silicon, instead of aliasing onto one flat store and coming "
166
+ "back right. NOT covered: which page lands in which bank — that "
167
+ "arithmetic is tt-metal's on both sides (host "
168
+ "WriteToDeviceInterleavedContiguous, device InterleavedAddrGen) and "
169
+ "never Wolfpine's — nor bank ordering, nor any claim about a "
170
+ "descriptor you supply yourself. Unlike the other entries here, "
171
+ "nothing raises and no environment variable disables it: this is a "
172
+ "modelling property, so it says the substrate your gate needs is "
173
+ "present, not that you will be told when your kernel is wrong."
174
+ ),
175
+ since="2026-08-19",
176
+ pinned_by=(
177
+ "framework.device.dram_banks_test:"
178
+ "test_every_bank_is_its_own_storage_at_its_own_coordinate"
179
+ ),
180
+ ),
181
+ Behaviour(
182
+ name="tensix-latched-wait-survives-stallwait",
183
+ guarantee=(
184
+ "A Tensix STALLWAIT issued while an earlier SEMWAIT or STALLWAIT "
185
+ "is latched and unsatisfied waits at its thread's Wait Gate "
186
+ "instead of replacing it, so a compute kernel's Dst handshake "
187
+ "holds however the LLK init calls are ordered around "
188
+ "tile_regs_wait(). STALLWAIT.md's block-mask table ticks "
189
+ "STALLWAIT in all nine columns, on both architectures, and it is "
190
+ "the only instruction whose row is; Wolfpine used to catch it by "
191
+ "its execution unit (Sync Unit, block bit B1) alone, so a "
192
+ "STALLWAIT behind a SEMWAIT whose block mask named the TDMA units "
193
+ "walked past and that semaphore wait was silently forgotten. "
194
+ "Concretely: pack_untilize_dest_init called after "
195
+ "tile_regs_wait() rather than before matmul_init dropped the "
196
+ "packer's wait on MATH_PACK, so a K >= 2 matmul packed a Dst that "
197
+ "was still mid-accumulation -- wrong values, no fault, and "
198
+ "correct on silicon and on ttsim. Like "
199
+ "dram-interleaved-bank-distinctness and unlike the raising "
200
+ "entries here, nothing throws and no environment variable "
201
+ "disables it: this is a modelling property, so it says the "
202
+ "ordering your kernel depends on is honoured, not that you will "
203
+ "be told when your kernel is wrong."
204
+ ),
205
+ since="2026-08-20",
206
+ pinned_by=(
207
+ "framework.pe.tensix.waitgate_stallwait_blocked_test:"
208
+ "test_a_stallwait_does_not_forget_an_unsatisfied_semwait"
209
+ ),
210
+ ),
211
+ Behaviour(
212
+ name="tensix-semaphore-bounds",
213
+ guarantee=(
214
+ "A Tensix SEMPOST that carries a semaphore to or past the Max its "
215
+ "own SEMINIT declared raises "
216
+ "framework.pe.tensix.semaphore_contract.SemaphoreContractError, "
217
+ "naming the semaphore, its value, that Max and the issuing thread; "
218
+ "so does a SEMPOST at 15 or a SEMGET at 0, on the Tensix "
219
+ "instructions and on the memory-mapped RISC-V path alike. Max is a "
220
+ "SEMWAIT C1 threshold and per SEMINIT.md 'has no effect on "
221
+ "SEMPOST', so hardware increments regardless and Wolfpine still "
222
+ "models that arithmetic — what the raise says is that the producer "
223
+ "issued past its own back-pressure, after which the computed "
224
+ "values depend on thread timing, which Wolfpine does not model. "
225
+ "Concretely: a compute kernel that hoists tile_regs_acquire() out "
226
+ "of its output-tile loop over-posts MATH_PACK the moment the "
227
+ "packer falls behind and wraps onto a Dst bank that has not been "
228
+ "drained; Wolfpine used to return the corrupted tiles while the "
229
+ "vendor simulator ttsim stopped. NOT covered: a post above Max on "
230
+ "a semaphore no SEMINIT configured, or through the memory-mapped "
231
+ "write, both of which working tt-metal kernels do and neither of "
232
+ "which raises. Set TT_SIM_DISABLE_SEMAPHORE_CHECKS=1 to restore "
233
+ "the old silence."
234
+ ),
235
+ since="2026-08-20",
236
+ pinned_by=(
237
+ "framework.pe.tensix.semaphore_contract_test:"
238
+ "test_a_sempost_past_a_declared_max_stops_and_names_everything"
239
+ ),
240
+ ),
241
+ Behaviour(
242
+ name="noc-transfer-alignment",
243
+ guarantee=(
244
+ "A NoC transfer whose source and destination addresses are not "
245
+ "congruent in the low bits the path requires raises "
246
+ "framework.network.alignment.NoCAlignmentError naming the path and "
247
+ "both addresses. Hardware neither faults nor completes such a "
248
+ "transfer correctly — it skews or drops the payload — so before "
249
+ "this a program with the defect produced plausible wrong numbers. "
250
+ "Set TT_SIM_DISABLE_ALIGNMENT_CHECKS=1 to switch it off, which "
251
+ "restores the old silence."
252
+ ),
253
+ since="2026-08-02",
254
+ pinned_by=(
255
+ "framework.network.alignment_test:"
256
+ "test_misaligned_transfer_raises_actionable_message"
257
+ ),
258
+ ),
259
+ )
260
+
261
+ #: name -> :class:`Behaviour`, for a consumer that wants to print or record the
262
+ #: whole set. Read-only: a suite must not be able to talk itself into a
263
+ #: guarantee by mutating the registry.
264
+ BEHAVIOURS = MappingProxyType({b.name: b for b in _BEHAVIOURS})
265
+
266
+
267
+ #: Exception classes in ``framework/`` that are deliberately *not* published as
268
+ #: behaviours, and why. The backward guard in ``behaviour_test.py`` requires
269
+ #: every exception class in the tree to appear here or in the registry above,
270
+ #: so this is the file where "we thought about it and the answer is no" is
271
+ #: written down rather than left implicit.
272
+ _NOT_A_GUARANTEE = MappingProxyType(
273
+ {
274
+ "UnsupportedBehaviour": (
275
+ "this registry's own failure mode, not a claim about the simulator"
276
+ ),
277
+ "NoC1ShadowError": (
278
+ "warn-by-default and opt-in via TT_SIM_NOC1_SHADOW, so a build "
279
+ "cannot promise it fires; it guards a device-construction mistake "
280
+ "rather than a program's behaviour"
281
+ ),
282
+ "NoCCoordinateError": (
283
+ "a request naming a coordinate outside the grid — a malformed "
284
+ "request rather than a modelling gap that used to pass"
285
+ ),
286
+ "NoCResponseError": (
287
+ "internal NoC bookkeeping; reaching it means Wolfpine is wrong, not "
288
+ "that the program under test is"
289
+ ),
290
+ "UnitWedgedError": (
291
+ "the deadlock watchdog. Worth publishing once the false-positive "
292
+ "rate is argued (v3 Strand B says so of every guard); until then a "
293
+ "consumer asserting on it would be asserting on a heuristic"
294
+ ),
295
+ "UnmodelledTileRegisterError": (
296
+ "names a register Wolfpine does not model. Honest, but the set it "
297
+ "covers moves with every release, so it is not a stable guarantee"
298
+ ),
299
+ "SrcDvalidError": (
300
+ "a Tensix Src-bank handshake violation: an internal invariant of "
301
+ "the coprocessor model, not something a kernel author can act on"
302
+ ),
303
+ "CSRError": "base class for the CSR errors below",
304
+ "NoCSRsError": (
305
+ "a core built without Zicsr meeting a CSR instruction — a "
306
+ "simulator configuration error, not a program defect"
307
+ ),
308
+ "UnknownCSRError": "an encoding no CSR answers for; malformed input",
309
+ "UnmodelledCSRError": (
310
+ "a real CSR Wolfpine does not implement. Same objection as "
311
+ "UnmodelledTileRegisterError: the covered set is not stable"
312
+ ),
313
+ "PlanError": (
314
+ "a malformed congestion-sweep plan; an offline analysis tool's "
315
+ "input validation, nowhere near the simulated device"
316
+ ),
317
+ }
318
+ )
319
+
320
+
321
+ class UnsupportedBehaviour(RuntimeError):
322
+ """This Wolfpine does not guarantee something the caller depends on."""
323
+
324
+
325
+ def supports(*names: str) -> bool:
326
+ """``True`` when every named behaviour is guaranteed by this build."""
327
+ return all(name in BEHAVIOURS for name in names)
328
+
329
+
330
+ def require(*names: str) -> None:
331
+ """Assert every named behaviour, or raise :class:`UnsupportedBehaviour`.
332
+
333
+ The failure names what is missing *and* what this build does have, because
334
+ the two most likely causes are an old checkout and a typo, and the reader
335
+ cannot tell which from a bare "not supported".
336
+ """
337
+ missing = [name for name in names if name not in BEHAVIOURS]
338
+ if not missing:
339
+ return
340
+ have = ", ".join(sorted(BEHAVIOURS)) or "(none)"
341
+ raise UnsupportedBehaviour(
342
+ f"this Wolfpine does not guarantee {', '.join(missing)}. Results from a "
343
+ f"suite that depends on it are not trustworthy — the failure mode these "
344
+ f"markers exist for is a green run that exercised nothing. This build "
345
+ f"guarantees: {have}. See {DOCS}."
346
+ )
347
+
348
+
349
+ def main(argv=None) -> int:
350
+ parser = argparse.ArgumentParser(
351
+ prog="python3 -m framework.behaviour",
352
+ description="What this Wolfpine guarantees, for an external test suite.",
353
+ )
354
+ parser.add_argument(
355
+ "--require",
356
+ metavar="NAME",
357
+ nargs="+",
358
+ default=(),
359
+ help="exit non-zero unless every NAME is guaranteed by this build",
360
+ )
361
+ parser.add_argument(
362
+ "--verbose",
363
+ action="store_true",
364
+ help="print each behaviour's guarantee, not just its name",
365
+ )
366
+ args = parser.parse_args(argv)
367
+
368
+ if args.require:
369
+ try:
370
+ require(*args.require)
371
+ except UnsupportedBehaviour as exc:
372
+ print(str(exc), file=sys.stderr)
373
+ return 1
374
+ return 0
375
+
376
+ for name in sorted(BEHAVIOURS):
377
+ if args.verbose:
378
+ behaviour = BEHAVIOURS[name]
379
+ print(f"{name} (since {behaviour.since})")
380
+ print(f" {behaviour.guarantee}")
381
+ else:
382
+ print(name)
383
+ return 0
384
+
385
+
386
+ if __name__ == "__main__":
387
+ raise SystemExit(main())
@@ -0,0 +1,212 @@
1
+ """The registry is held to the code from both directions.
2
+
3
+ A published capability marker is only worth what its maintenance is worth. The
4
+ failure it exists to prevent — a consumer's suite running green against a
5
+ simulator that silently does nothing — is exactly the failure a stale registry
6
+ reintroduces, one level up: assert on ``noc1-multicast-corner-order``, get a
7
+ pass, and have the name mean nothing because the guard behind it was deleted
8
+ two refactors ago.
9
+
10
+ So:
11
+
12
+ * **Forward** — every entry names a test, and that test has to exist. An entry
13
+ whose guarantee is no longer checked anywhere cannot survive here.
14
+ * **Backward** — every exception class in ``framework/`` has to be either
15
+ registered or explicitly declined in ``_NOT_A_GUARANTEE``. This is the guard
16
+ that stops the scheme decaying, because the omission people actually make is
17
+ landing a new loudness guard and never telling anyone outside about it. The
18
+ scan is over the source tree rather than a list, so it covers modules nobody
19
+ thought about when this was written.
20
+
21
+ Runs standalone (``python3 -m framework.behaviour_test``) or under pytest.
22
+ """
23
+
24
+ import ast
25
+ import importlib
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ import pytest
31
+
32
+ from framework.behaviour import (
33
+ _NOT_A_GUARANTEE,
34
+ BEHAVIOURS,
35
+ UnsupportedBehaviour,
36
+ main,
37
+ require,
38
+ supports,
39
+ )
40
+
41
+ _ROOT = Path(__file__).resolve().parent
42
+
43
+
44
+ def _exception_classes():
45
+ """``(class name, path)`` for every exception defined under ``framework/``.
46
+
47
+ Parsed rather than imported: importing the whole tree to enumerate its
48
+ exceptions would make a registry check the slowest test in the suite, and
49
+ would drag in numpy, pyarrow and the device model to answer a question that
50
+ is answerable from the text. A class counts as an exception if its own name
51
+ or any base's ends in ``Error`` or ``Exception``, which is the naming this
52
+ repo (and the standard library) keeps to.
53
+ """
54
+ out = []
55
+ for path in sorted(_ROOT.rglob("*.py")):
56
+ if path.name.endswith("_test.py") or "__pycache__" in path.parts:
57
+ continue
58
+ tree = ast.parse(path.read_text(), filename=str(path))
59
+ for node in ast.walk(tree):
60
+ if not isinstance(node, ast.ClassDef):
61
+ continue
62
+ names = [node.name]
63
+ for base in node.bases:
64
+ if isinstance(base, ast.Name):
65
+ names.append(base.id)
66
+ elif isinstance(base, ast.Attribute):
67
+ names.append(base.attr)
68
+ if any(n.endswith(("Error", "Exception")) for n in names):
69
+ out.append((node.name, path.relative_to(_ROOT.parent)))
70
+ return out
71
+
72
+
73
+ # --- forward: an entry cannot outlive the test that checks it ---------------
74
+
75
+
76
+ def test_every_behaviour_names_a_test_that_exists():
77
+ assert BEHAVIOURS, "the registry is empty"
78
+ for name, behaviour in BEHAVIOURS.items():
79
+ module_name, _, function = behaviour.pinned_by.partition(":")
80
+ assert function, f"{name}: pinned_by must be 'module:function'"
81
+ module = importlib.import_module(module_name)
82
+ assert callable(getattr(module, function, None)), (
83
+ f"{name} claims to be pinned by {behaviour.pinned_by}, which does "
84
+ f"not exist. Either the test was renamed (repoint it) or the "
85
+ f"guarantee stopped being checked (drop the entry)."
86
+ )
87
+
88
+
89
+ def test_every_behaviour_is_completely_described():
90
+ for name, behaviour in BEHAVIOURS.items():
91
+ assert name == behaviour.name
92
+ assert name == name.lower(), f"{name}: keep names lowercase"
93
+ assert " " not in name, (
94
+ f"{name}: names are typed into other people's test files; keep "
95
+ f"them hyphenated so they survive being pasted anywhere"
96
+ )
97
+ assert (
98
+ len(behaviour.guarantee) > 60
99
+ ), f"{name}: the guarantee has to say what a run will observably do"
100
+ assert len(behaviour.since) == 10, f"{name}: since must be YYYY-MM-DD"
101
+ assert behaviour.since.count("-") == 2, f"{name}: since must be YYYY-MM-DD"
102
+
103
+
104
+ # --- backward: a new guard cannot land unmentioned ---------------------------
105
+
106
+
107
+ def test_every_exception_is_registered_or_explicitly_declined():
108
+ """The guard against decay. A new exception class in ``framework/`` turns this
109
+ red until somebody decides, in writing, whether an outside consumer would
110
+ want to assert on it."""
111
+ # A behaviour accounts for an exception only by *naming* it in the
112
+ # guarantee text, so the registry cannot quietly claim a class without
113
+ # telling a reader which one it means.
114
+ guarantees = [behaviour.guarantee for behaviour in BEHAVIOURS.values()]
115
+ unaccounted = [
116
+ (name, path)
117
+ for name, path in _exception_classes()
118
+ if name not in _NOT_A_GUARANTEE
119
+ and not any(name in guarantee for guarantee in guarantees)
120
+ ]
121
+ assert not unaccounted, (
122
+ "these exception classes are neither named by a registered behaviour "
123
+ "nor declined in framework/behaviour.py's _NOT_A_GUARANTEE: "
124
+ + ", ".join(f"{name} ({path})" for name, path in unaccounted)
125
+ + ". If an external suite would want to assert that this build raises "
126
+ "it, register a behaviour; if not, add it to _NOT_A_GUARANTEE with the "
127
+ "reason."
128
+ )
129
+
130
+
131
+ def test_nothing_is_declined_that_no_longer_exists():
132
+ """The exemption list is checked in the same direction as the registry, so
133
+ it cannot accumulate names for classes that were deleted years ago."""
134
+ defined = {name for name, _ in _exception_classes()}
135
+ stale = sorted(set(_NOT_A_GUARANTEE) - defined)
136
+ assert not stale, f"_NOT_A_GUARANTEE names classes that do not exist: {stale}"
137
+
138
+
139
+ def test_every_reason_given_is_a_reason():
140
+ for name, reason in _NOT_A_GUARANTEE.items():
141
+ assert len(reason) > 20, f"{name}: 'why not' needs an actual answer"
142
+
143
+
144
+ # --- the consumer-facing surface --------------------------------------------
145
+
146
+
147
+ def test_the_snippet_a_consumer_writes():
148
+ """Verbatim from the module docstring and the docs section."""
149
+ require("noc1-multicast-corner-order")
150
+ assert supports("noc1-multicast-corner-order")
151
+
152
+
153
+ def test_an_absent_behaviour_fails_loudly_and_usefully():
154
+ with pytest.raises(UnsupportedBehaviour) as caught:
155
+ require("noc1-multicast-corner-order", "no-such-guarantee")
156
+ message = str(caught.value)
157
+ assert "no-such-guarantee" in message
158
+ # Not merely "unsupported": the reader is told what this build does have,
159
+ # because a typo and an old checkout look identical otherwise.
160
+ assert "noc1-multicast-corner-order" in message
161
+ assert "docs/running-tt-metal-on-the-simulator.md" in message
162
+ assert not supports("no-such-guarantee")
163
+ assert not supports("noc1-multicast-corner-order", "no-such-guarantee")
164
+
165
+
166
+ def test_the_registry_cannot_be_talked_into_a_guarantee():
167
+ with pytest.raises(TypeError):
168
+ BEHAVIOURS["invented"] = None
169
+
170
+
171
+ def test_the_cli_lists_and_checks(capsys):
172
+ assert main([]) == 0
173
+ listed = capsys.readouterr().out.split()
174
+ assert listed == sorted(BEHAVIOURS)
175
+
176
+ assert main(["--require", *sorted(BEHAVIOURS)]) == 0
177
+ assert main(["--require", "no-such-guarantee"]) == 1
178
+ assert "no-such-guarantee" in capsys.readouterr().err
179
+
180
+
181
+ def test_the_cli_works_for_a_consumer_with_no_python_at_all():
182
+ """A C++ harness or a Makefile shells out; that path must exit non-zero
183
+ on a missing behaviour rather than printing something a script has to
184
+ parse."""
185
+ ok = subprocess.run(
186
+ [sys.executable, "-m", "framework.behaviour", "--require"] + sorted(BEHAVIOURS),
187
+ capture_output=True,
188
+ text=True,
189
+ cwd=_ROOT.parent,
190
+ )
191
+ assert ok.returncode == 0, ok.stderr
192
+ bad = subprocess.run(
193
+ [sys.executable, "-m", "framework.behaviour", "--require", "no-such-guarantee"],
194
+ capture_output=True,
195
+ text=True,
196
+ cwd=_ROOT.parent,
197
+ )
198
+ assert bad.returncode == 1
199
+ assert "no-such-guarantee" in bad.stderr
200
+
201
+
202
+ def main_standalone():
203
+ for name, fn in sorted(globals().items()):
204
+ if name.startswith("test_") and callable(fn):
205
+ if fn.__code__.co_argcount:
206
+ continue # needs a pytest fixture
207
+ fn()
208
+ print("behaviour tests OK")
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main_standalone()
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wolfpine"
7
+ version = "2.1"
8
+ description = "A framework for simulating AI accelerators, with Tenstorrent Wormhole and Blackhole backends"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "BSD-3-Clause"
12
+ authors = [ { name = "Nick Brown" } ]
13
+ classifiers = ["Programming Language :: Python :: 3"]
14
+ dependencies = [
15
+ "numpy",
16
+ "pyyaml",
17
+ "pynng",
18
+ "flatbuffers",
19
+ "pyarrow",
20
+ "pyelftools",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev=["ruff", "pytest"]
25
+
26
+ [tool.setuptools]
27
+ packages = ["framework"]
28
+
29
+ [tool.ruff]
30
+ target-version = "py310"
31
+ extend-include = ["*.ipynb", "*.pyi"]
32
+ exclude = [".venv", "docs/marimo", "framework/bridge/_flatbuf"]
33
+
34
+ [tool.ruff.lint]
35
+ select = ["E", "F", "W", "I", "UP", "PT", "TID251", "INP", "PYI"]
36
+ ignore = [
37
+ "E741", # https://beta.ruff.rs/docs/rules/ambiguous-variable-name/
38
+ "PT006", # https://beta.ruff.rs/docs/rules/pytest-parametrize-names-wrong-type/
39
+ "PT012", # https://beta.ruff.rs/docs/rules/pytest-raises-with-multiple-statements/
40
+ "PYI041", # https://docs.astral.sh/ruff/rules/redundant-numeric-union
41
+ ]
42
+
43
+ [tool.ruff.lint.pycodestyle]
44
+ max-line-length = 130
wolfpine-2.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: wolfpine
3
+ Version: 2.1
4
+ Summary: A framework for simulating AI accelerators, with Tenstorrent Wormhole and Blackhole backends
5
+ Author: Nick Brown
6
+ License-Expression: BSD-3-Clause
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pyyaml
13
+ Requires-Dist: pynng
14
+ Requires-Dist: flatbuffers
15
+ Requires-Dist: pyarrow
16
+ Requires-Dist: pyelftools
17
+ Provides-Extra: dev
18
+ Requires-Dist: ruff; extra == "dev"
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # Wolfpine
23
+
24
+ **A framework for simulating AI accelerators, in pure Python.**
25
+
26
+ Wolfpine is a hackable software framework for building cycle-approximate
27
+ simulators of AI accelerator hardware. The core — the memory map, the
28
+ interconnect, the RISC-V cores, the clock and reset fabric, the cost and
29
+ trace machinery — is architecture-agnostic; a target is described by an
30
+ *architecture profile* plus a thin device class, not by a fork.
31
+
32
+ The framework ships with backends for the Tenstorrent architecture,
33
+ **Wormhole** and **Blackhole**, built from the public
34
+ [Tenstorrent ISA Documentation](https://github.com/tenstorrent/tt-isa-documentation/tree/main).
35
+ Both run real tt-metal programs unmodified, on the full default worker grid —
36
+ 72 workers on Wormhole, 130 on Blackhole. Note that it is not cycle accurate
37
+ (or intended to be). The idea was to build a simulator from public
38
+ documentation, so developers can experiment with writing accelerator code
39
+ without needing physical hardware — and, because it is Python all the way
40
+ down, poke at simulator state directly from a driver script.
41
+
42
+ ## Contents
43
+
44
+ - [Installation](#installation)
45
+ - [Using Wolfpine as a simulator for tt-metal](#using-wolfpine-as-a-simulator-for-tt-metal)
46
+ - [Repository overview](#repository-overview)
47
+ - [What the timing model claims, and what it does not](#what-the-timing-model-claims-and-what-it-does-not)
48
+ - [Key parts of the simulator](#key-parts-of-the-simulator)
49
+
50
+ Profiling a kernel: [`driver/wormhole/docs/profiling.md`](driver/wormhole/docs/profiling.md)
51
+ walks through every output the simulator can produce.
52
+ Building tooling on those outputs: [`docs/trace-schema.md`](docs/trace-schema.md)
53
+ is the stable, versioned schema — field meanings, units and what you may
54
+ rely on.
55
+
56
+ ## Installation
57
+
58
+ Wolfpine is pure Python and requires Python ≥ 3.10. Clone the repository and install it in
59
+ editable mode, which pulls in its dependencies (numpy, pyyaml, pynng, flatbuffers,
60
+ pyarrow, pyelftools):
61
+
62
+ ```bash
63
+ git clone https://github.com/hedgehope/wolfpine.git
64
+ cd wolfpine
65
+ pip install -e . # add [dev] for the ruff + pytest tooling: pip install -e .[dev]
66
+ ```
67
+
68
+ This installs the `framework` simulator library. The example drivers live under `driver/`
69
+ and are run from the repository root, so keep the repo root on your `PYTHONPATH` when
70
+ invoking them directly (the tt-metal flow below wires this up for you automatically):
71
+
72
+ ```bash
73
+ export PYTHONPATH=$PWD:$PYTHONPATH
74
+ ```
75
+
76
+ ## Using Wolfpine as a simulator for tt-metal
77
+
78
+ tt-metal's UMD has a "simulation" chip backend: point `TT_METAL_SIMULATOR` at the
79
+ [`driver/wormhole`](driver/wormhole) directory and UMD launches Wolfpine in place of real
80
+ silicon, so an ordinary tt-metal program runs against the simulator over a socket — no
81
+ code changes, exactly as it would run on hardware. The minimal setup:
82
+
83
+ ```bash
84
+ export TT_METAL_RUNTIME_ROOT=/path/to/tt-metal # your built tt-metal checkout
85
+ export TT_METAL_SIMULATOR="$PWD/driver/wormhole" # the switch: route execution to Wolfpine
86
+ export TT_METAL_SLOW_DISPATCH_MODE=1 # the only launch path the sim models
87
+ export LD_LIBRARY_PATH="$TT_METAL_RUNTIME_ROOT/build/lib:$LD_LIBRARY_PATH"
88
+
89
+ # then run any tt-metal program, e.g. an upstream example:
90
+ cd "$TT_METAL_RUNTIME_ROOT/build/programming_examples"
91
+ ./metal_example_add_2_integers_in_compute
92
+ ```
93
+
94
+ A set of ready-to-run example programs, plus a build-and-run test suite, lives under
95
+ [`examples`](examples). For the full walkthrough — building the
96
+ examples, the `TT_SIM_TENSIX_COORDS` grid-pinning knob, diagnostics, structured tracing and
97
+ the deadlock watchdog — see **[`driver/wormhole/README.md`](driver/wormhole/README.md)**.
98
+
99
+ ## Repository overview
100
+
101
+ The simulator implementation is in the [framework](https://github.com/hedgehope/wolfpine/tree/main/framework) directory, with the [driver](https://github.com/hedgehope/wolfpine/tree/main/driver) directory providing a range of examples that illustrate running the simulator. These are individually documented, but to summarise:
102
+
103
+ * [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) is where you likely want to go to. It provides an implementation of a Wormhole, currently with one DRAM tile and one Tensix block (although this is easy to expand, although will likely be slow!) It holds a set of example tt-metal programs that are built against a local tt-metal checkout and run against the simulator — exactly as they would run on hardware, only the device is the simulator. Each validates its own results, so the examples double as a test suite. See its README for how to build and run them.
104
+ * [blackhole](https://github.com/hedgehope/wolfpine/tree/main/driver/blackhole) is the Blackhole driver: the same wire bridge and machinery as wormhole (all shared in [`framework/bridge`](https://github.com/hedgehope/wolfpine/tree/main/framework/bridge)), pointed at a Blackhole device instead. Point `TT_METAL_SIMULATOR` at `driver/blackhole` to route execution there. Workers are materialised on demand by the wire bridge, so a program gets exactly the grid it launches on, up to the full 130; see its README.
105
+ * [simple](https://github.com/hedgehope/wolfpine/tree/main/driver/simple) are very basic examples, demonstrating the memory subsystem and running codes on a vanilla RV32IM CPU.
106
+
107
+ ### What the timing model claims, and what it does not
108
+
109
+ The simulator carries a cost model of **documented provenance**: every cycle
110
+ figure is traceable to the ISA documentation or a vendor source, there are
111
+ **zero un-sourced estimates** (asserted by test), and it has been checked
112
+ against real silicon. Stated precisely:
113
+
114
+ > Runs real tt-metal programs unmodified on both architectures, on the full
115
+ > default grid, with a timing model of documented provenance, corroborated
116
+ > against silicon at the **slope** and **launch** level on both parts, and
117
+ > **by mechanism** for NoC-bound work on both parts.
118
+
119
+ Four caveats belong with that sentence rather than under it, because three of
120
+ them are permanent:
121
+
122
+ - **It is a floor.** Every bound is charged at its low end, by rule, so it
123
+ under-predicts wherever hardware sits above a documented minimum.
124
+ - **Mechanism-level checking is one leg of three.** The RV-bound leg is
125
+ Blackhole-only (Wormhole documents no CSRs, so there is no retired-instruction
126
+ count) and currently fails its bar; the Tensix mechanism leg cannot be built
127
+ at all from the counters the hardware exposes.
128
+ - **That failure has a single, sourced cause** — integer divide, where the
129
+ documentation gives only a 6–33 cycle range and no algorithm, so a
130
+ magnitude-dependent term cannot be sourced at any provenance.
131
+ - **Energy is ranking-level only**, and barely beats a model that knows nothing
132
+ but the cycle count. Absolute joules are out of reach of the instrument.
133
+
134
+ The full evidence, including what was proven *impossible* rather than merely
135
+ left undone, is in [`ROADMAP.md`](ROADMAP.md).
136
+
137
+ Optional diagnostic information can be provided by each RISC-V baby core, the NoC, the Tensix co-processor and memory, enabling tracing of the execution of a program. This is currently at the instruction and architectural state level, but could be enhanced in the future to provide feedback to developers around potential code bottlenecks or other issues.
138
+
139
+ This is written in Python, mainly to make it easy for people to hackaround and experiment with things. If you want to add some functionality, or fix a bug, then please feel free to go ahead and raise a PR. This is still work in progress, so also please raise issues etc as you find them!
140
+
141
+ ## Key parts of the simulator
142
+
143
+ There are a few of key components which are worth highlighting:
144
+
145
+ * [tt_device.Wormhole](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L111) creates the Wormhole, currently with a single DRAM tile and a tensix tile. Here you can see the NoC coordinates specified of each, and also the booleans provided to _TensixTile_ are whether to report diagnostic information (the first five for each RISC-V baby core, then next two for each NoC and then the separate Tensix co-processor choices). In the [wormhole](https://github.com/hedgehope/wolfpine/tree/main/driver/wormhole) tt-metal flow these are set from the `TT_SIM_DIAG_*` environment variables (see that directory's README).
146
+ * [tt_device.TensixTile](https://github.com/hedgehope/wolfpine/blob/93da242e8a1a26160afaca43b0772bebc88b9171/tt_sim/device/tt_device.py#L186) plumbs everything together within a Tensix tile, setting all the memory addresses and ranges for each individual component. These are all based on the ISA documentation [memory map](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/README.md).
147
+ * [tt_noc](https://github.com/hedgehope/wolfpine/blob/main/framework/network/tt_noc.py) is the implementation of the NoC, it is not yet complete with all the functionality but is sufficient to communicate between tiles and, for example, read and write between DRAM and the Tensix tile.
148
+ * [rv](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/rv) provides the RV32IM implementation with [.ttinsn extension](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/BabyRISCV/PushTensixInstruction.md#ttinsn-instruction-set-extension). This is fairly self explanatory, providing a pluggable approach to combining ISAs. The [BabyRISCV](https://github.com/hedgehope/wolfpine/blob/main/framework/pe/rv/babyriscv.py) ties this together for the baby RISC-V cores in the Tensix tile, for instance determining the initial PC value after a soft reset as per [here](https://github.com/tenstorrent/tt-isa-documentation/blob/main/WormholeB0/TensixTile/SoftReset.md).
149
+ * [tensix](https://github.com/hedgehope/wolfpine/tree/main/framework/pe/tensix) is the implementation of the Tensix coprocessor as per [here](https://github.com/tenstorrent/tt-isa-documentation/tree/main/WormholeB0/TensixTile/TensixCoprocessor). This is not fully complete, but enough to run a range of codes that use the matrix, vector and scalar units (and all the associated unit implementation required to enable this).
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ framework/__init__.py
5
+ framework/behaviour.py
6
+ framework/behaviour_test.py
7
+ wolfpine.egg-info/PKG-INFO
8
+ wolfpine.egg-info/SOURCES.txt
9
+ wolfpine.egg-info/dependency_links.txt
10
+ wolfpine.egg-info/requires.txt
11
+ wolfpine.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ numpy
2
+ pyyaml
3
+ pynng
4
+ flatbuffers
5
+ pyarrow
6
+ pyelftools
7
+
8
+ [dev]
9
+ ruff
10
+ pytest
@@ -0,0 +1 @@
1
+ framework