File size: 5,594 Bytes
ecc81b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# Adding an nd_method

A [mixer](adding-a-mixer.md) decides what happens *along one axis*. An
**nd_method** decides how the axes are handled at all: which layer gets which
axis, whether axes are swept one at a time or contracted together, and whether
some axes belong to a different operator entirely.

This is the argument the library exists to make one flag:

```python
td.LSTM(64, 12, lattice, method=td.axial_scan)  # sweep every axis
td.LSTM(64, 12, lattice, method=td.cafa)  # kernels across space, RNN along time
td.LSTM(64, 12, lattice, method=my_strategy)  # yours, no registration
```

The worked example is [`examples/custom_method.py`](../examples/custom_method.py),
executed by `tests/test_examples.py`.

---

## The contract

```python
def nd_method(mixer, plan, lattice, d_model, **kwargs) -> nn.Module: ...
```

- `mixer` is a **factory**, not a module β€” call it to build one. A strategy
  decides how many mixers exist and whether they are shared, so it cannot be
  handed a single instance. It may be `None`: the kernel family accepts no
  mixer on a lattice with no time axis, because there would be nothing for it
  to sweep.
- `plan` is the requested schedule, as data. You may use it, ignore it, or
  rewrite it.
- `lattice` and `d_model` are what you are working over and how wide.
- `**kwargs` carries whatever the model was constructed with (`dropout`,
  `chunk`, and anything strategy-specific like `gate=`).

A strategy is a plain function rather than a class because not all of them wrap
a single module β€” the hybrid strategies compose two different operators over
different axes.

## Three shapes a strategy can take

**1. Rewrite the schedule, delegate the rest.** The cheapest useful thing, and
only possible because a plan is a value:

```python
def pyramid(mixer, plan, lattice, d_model, **kwargs):
    """Sweep the largest axis every other layer; rotate the rest through."""
    order = [lattice.axis_names[lattice.axis_index(s.axis)] for s in plan]
    biggest = max((a for a in order if a != "time"), key=lattice.axis_size)
    ...
    return td.axial_scan(mixer, td.ScanPlan.from_list(steps), lattice, d_model, **kwargs)
```

**2. Compose two operators over different axes.** This is what `td.cafa` and
`td.axial_attention` do: per-axis kernels own the spatial axes, the model's
mixer owns time. Nothing stops a strategy from using attention on one axis, a
scan on another, and nothing at all on a third.

**3. Build something entirely your own.** The return value only has to be an
`nn.Module` mapping `(B, [T,] *shape, H)` to the same shape. `axial_apply` and
`axial_contract` are exported so a new strategy can reuse the fold rather than
reimplement it.

## Run the conformance suite β€” it will find things

`check_block` takes a strategy exactly as it takes a mixer:

```python
def factory(lat, d_model, plan=None):
    return td.LSTM(d_model, len(lat.axis_names) + 1, lat, plan=plan, method=pyramid)


report = td.testing.check_block(factory)
```

**Both bugs in the example above were found this way, not by reading it.** The
first version of `pyramid` failed two checks:

```
[FAIL] shape is preserved β€” ZeroDivisionError: integer division or modulo by zero
[FAIL] output is covariant with axis storage order β€” output depends on the order
       axes happen to be stored in, not just on the sweep order
```

- **The division by zero** was `others[(i // 2) % len(others)]` on a rank-1
  lattice, where the only axis *is* the dominant one and `others` is empty.
  Caught by the cheapest check in the suite, at the rank most people skip.
- **The covariance failure** was subtler and is the classic N-D bug in one
  line: the strategy took its axis order from `lattice.axis_names`, so the same
  model over the same data laid out differently produced a *different
  schedule*. A model whose behaviour depends on storage order rather than on
  the sweep order it was asked for is wrong in a way that no loss curve will
  ever show you. The fix is to order axes by the plan, which is
  storage-independent.

If you write a strategy that rewrites schedules, assume you have made one of
these two mistakes until the report says otherwise.

## Inspect what your strategy actually did

```python
print(model.nd.plan.coverage(lattice))
# Coverage(8 layers)
#   time    2β†’   0←  forward
#   row     0β†’   2←  backward
#   col     2β†’   2←  both
```

`coverage` is the machine-readable answer to "did every axis get swept, and in
both directions". Here `row` came out **backward only** β€” a real property of
this toy schedule, invisible in its source, and exactly the class of bug
recorded as DEBUG.md #4 (a published implementation whose every axis was
silently pinned to one direction). Assert on coverage in your own tests:

```python
assert cov["time"].backward == 0  # time must stay causal
assert not cov.unswept  # every axis gets mixing
```

## Register it (only for config files)

```python
td.register_nd_method("pyramid", pyramid)
td.build({"kind": "lstm", "d_model": 16, "n_layers": 6, "method": "pyramid", ...})
```

Registration exists because YAML cannot hold a callable. In Python, pass the
function. A checkpoint of a model built with an *unregistered* strategy refuses
to save, with a message saying why β€” a checkpoint that cannot name its own
strategy could never rebuild itself.

## Next

- [Adding a mixer](adding-a-mixer.md)
- [DESIGN.md](../DESIGN.md) β€” why mixer and method are separate concepts
- [BENCHMARKS.md](../BENCHMARKS.md) β€” what each shipped strategy costs, and
  where the factorized one starts to win