# Recurrent state

Convert a stateful LSTM and inspect its persistent arena. The generated module
retains recurrent state between calls to `aot_model_run()` and resets it during
model initialization.

| Field | Value |
| --- | --- |
| Model | one UNIDIRECTIONAL_SEQUENCE_LSTM node, int8 activations, int16 cell state |
| Target | `apollo510_evb` |
| Shows | LiteRT variable tensors as persistent arenas, and what `test.state_feedback` is for |
| CI | Conversion and host compilation are declared in the examples CI job |

The model is built by [make_model.py](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/recurrent-state/make_model.py) rather than fetched; see
[Model provenance](https://ambiqai.github.io/helia-aot/examples/) for why no zoo model is used here.

## Prerequisites

Use a repository checkout and activate its environment after `uv sync --frozen --group ci`. The model-building script imports the repository's Python helpers; an isolated CLI installation alone is not enough. See [Running examples](https://ambiqai.github.io/helia-aot/examples/).

## Run it

```sh
./run.sh
```

[config.yaml](https://github.com/AmbiqAI/helia-aot/blob/cf2246a7ad439fa38d57462118e7840e5497a685/examples/recurrent-state/config.yaml) asks for nothing about state. The model's variable
tensors are what produce it.

## What to look at

The arena summary grows a third line:

```text
 Region   Kind                          Size    Capacity
 dtcm     scratch                       56 B    512.0 KB
 dtcm     persistent                    24 B    512.0 KB
 dtcm     constant (cold) · 21          96 B    512.0 KB
```

Scratch is reused within a run. Persistent is not: the output state and the
cell state are LiteRT variable tensors, and the module keeps them across runs
so a streaming caller can hand it one window at a time. The residency report
in `out/lstm/aot_residency.json` names both tensors and the arena they landed
in.

The generated module has shared arena storage: two context structs do **not**
carry independent state. Serialize calls to one logical module instance.
`model_init()` resets persistent storage, while subsequent successful
`model_run()` calls carry it. For separate streams, use separately generated
modules with distinct prefixes and independent persistent storage, or design
and validate explicit state save/restore; a second struct alone is insufficient.

## The other shape of state

The following is a configuration fragment for a **different model** with
explicit state I/O, not a command to add to this LSTM recipe. This recipe does
not create `golden.npz` or expose the illustrated feedback pair.

Some recurrent models do not hold state internally: they expose the initial
state as an input and the final state as an output, and the caller is expected
to feed one back into the other. For those, `test.state_feedback` tells the
generated test harness to do exactly that, as a list of output-index to
input-index pairs:

```yaml
test:
  enabled: true
  golden_data: golden.npz
  num_iterations: 4
  state_feedback: [[0, 1]]
```

The harness then seeds the state once and carries each run's output into the
next run's input. The NPZ supplies one `input_N` array per input and one
`output_N` array per output, with the model's dtype and shape. It does not
contain a per-iteration sequence: `output_N` must represent the final result
after `num_iterations` with the same reset/carry policy. Intermediate outputs
are not compared. Choose indices for that model's real I/O. Without
feedback, the harness refreshes ordinary inputs from the fixture; an internal
variable-state model can still carry state. Compare against a trusted oracle
using the same reset/carry policy.

## Next

- [How heliaAOT works](https://ambiqai.github.io/helia-aot/guide/how-it-works/)
  for where planning decides arena roles.
- [Validate it on target](https://ambiqai.github.io/helia-aot/getting-started/validate/)
  for the golden harness this configuration feeds.
