Stateful and streaming models¶
Models that keep state between calls (streaming audio, RNNs, causal convolutions with a cache, transformers with a KV cache) have a second code path: the chunked one. It has to agree with the full pass, a reset has to really reset, and one request must never leak into another.
from pytest_torch import assert_streaming_equivalent
def test_streaming(denoiser, make_tensor):
assert_streaming_equivalent(
full_runner=denoiser.forward,
streaming_runner=denoiser.forward_chunk,
inputs=make_tensor(2, 50, 8), # (batch, time, features)
chunk_sizes=[1, 7, 32],
reset=denoiser.reset_cache,
)
Streaming: 3 passed, 2 failed
[PASS] chunked[1]
[FAIL] chunked[7]
chunk size 7: streamed output differs from the full pass
output: 12/400 elements differ beyond tolerance (rtol=1e-05, atol=1e-05)
...
[FAIL] reset
after a reset, streaming the same input again gave different outputs (state survives the reset)
The checks¶
| Check | Passes when |
|---|---|
chunked[n] |
Feeding the sequence in chunks of n and concatenating the outputs agrees with the full pass, under the chosen equivalence |
reset |
After a reset (or a fresh init_state()), streaming the same input again gives exactly the same outputs |
isolation |
A request isn't contaminated by another one (see below) |
Select with checks=["chunked", "reset", "isolation"]; raise_on_failure=False returns the
StreamingReport instead of raising.
Two kinds of state¶
Internal state: the model keeps its cache and has a reset method. Pass reset=. The
streaming runner is called like a model: streaming_runner(chunk) (or with *args /
**kwargs for tuple and dict inputs). Isolation runs another request, resets, then
checks that the original request gives the same result as on its own.
Explicit state: a pure step function, streaming_runner(chunk, state) -> (output,
new_state), and init_state= returning an empty state. Isolation is stronger here: two
requests are interleaved chunk by chunk, each with its own state, like a server handling
several streams at once, and each must equal its isolated run. That catches state shared
through a module attribute or a global.
A KV cache is explicit state, so chunked prefill can be tested directly:
def prefill(chunk, past):
out = lm(chunk, past=past, use_cache=True)
return out.logits, out.past
assert_streaming_equivalent(lambda ids: lm(ids).logits, prefill, ids,
chunk_sizes=[1, 3, 8], init_state=lambda: None)
Inputs and outputs¶
- Inputs are split along
seq_dim(default 1, batch-first). Tensors of another length, such as a speaker embedding, go to every chunk whole; the sequence length is the longest size alongseq_dim. - Chunk outputs whose length matches the chunk are concatenated along
seq_dim; other outputs (a pooled summary, a final state) are taken from the last chunk. Passcombine=to combine the list of chunk outputs yourself. - The second request for the isolation check is the inputs reversed in time, or
other_inputs=.
When chunking intentionally changes the output¶
Some models behave differently when chunked by design: look-ahead, chunk-local attention,
warm-up. Declare the expected equivalence with equivalence=:
from pytest_torch import StreamingEquivalence
# each chunk's last 2 frames wait for future context
equivalence=StreamingEquivalence(skip_last=2)
# also: skip_first=, skip_warmup= (start of the whole sequence), rtol=, atol=
Or pass a callable (full_output, streamed_output, chunk_size) that raises
AssertionError (or returns False) on a mismatch, or a dict keyed by chunk size, with
"default" for the others:
equivalence={
1: no_future_at_all, # 1-frame chunks: a different, known behavior
"default": StreamingEquivalence(skip_last=2),
}
The default tolerance is the streaming check's (1e-5), configurable in
pyproject.toml. The reset and isolation checks compare exactly: the same
input and the same starting state must give bit-identical outputs.