A multi-stream variant of Qwen3.5-27B (DeltaNet hybrid) that generates in ten
parallel streams (1 input, 1 visible output, 8 thinking channels) simultaneously per timestep. One forward pass
produces the next-row token for each stream; tokens within a row cannot see
each other (block-causal attention), but every stream can attend to every
prior row's tokens.
This model was trained for the monitoring experiments in Section 7 and to see whether whether we can train a generic instruction-tuned model with 8 internal streams, and still have it make sense.
As such, the internal streams are not always helpful, but they are coherent, and do (in the best case) respond to each other and the user stream. Nevertheless this is still a research prototype model.
Channel embedding: 10 learned vectors added to token embeddings,
identifying which channel each token belongs to.
Block-causal attention: For each row, all C=10 tokens see prior rows and
themselves but never their same-row peers. Implemented with a custom 4D mask
and column-mode masking on the GatedDeltaNet conv1d and recurrence.
Loss / inference: Shift-by-10 next-row prediction. Inference forwards one
row (10 tokens) per step and reuses the KV / DeltaNet cache.
Channels
#
Name
Role
0
User
Input stream (input stream, filled per step)
1
Output
Visible output
2
Analytical
Forward-facing planning
3
Skeptical
Backward-facing validation
4
Intuitive
Present-moment felt-sense
5
Between
Relational awareness
6
Curious
Generative questioning
7
Void
Associations, daydreaming
8
Instinct
Pragmatic constraints
9
Synthesis
Meta-level integration
Silence token: - → token id 481 in the Qwen3.5 tokenizer (used when a
channel has nothing to say on a given row).
trust_remote_code=True is required as the bundled modeling_qwen3_5.py
wires up channel embeddings, block-causal masking, and DeltaNet state
forwarding.
Stream-style generation
The model exposes two convenience methods directly on the loaded module:
stream_generate(...) returns a StreamResult dataclass with:
Attribute
Type
Notes
result.tokens
list[list[int]]
Shape [num_rows, 10] of raw token ids.
result.channel_texts[name]
dict[str, str]
Decoded text per stream (silence stripped).
result.output
str
Shortcut for stream["Output"].
result.num_rows
int
result.silence_ratio(name)
float
Fraction of rows the stream produced silence.
For grid rendering / interactive demos, use the generator form:
python
1for row_idx, row, is_prefill in model.stream_generate_iter(2 tokenizer,3"Hello, what's up?",4 max_rows=80,5 warm_start=True,6 silence_penalty=5.0,7 skip_silence=True,8):9 cells =[tokenizer.decode([t]).strip()or"-"for t in row]10print(f"{row_idx:3d} "+" | ".join(c[:10].ljust(10)for c in cells))
Interactive mode (send user tokens mid-generation)
Pass an empty prompt to enter interactive mode where the generator then accepts
.send(token_id) calls so user input is injected one token at a time while
the other nine channels keep producing:
python
1gen = model.stream_generate_iter(tokenizer,"", silence_penalty=5.0, max_rows=10_000)23# Drain any prefill rows so the generator suspends at the first .send point.4row_idx, row, is_prefill =next(gen)5while is_prefill:6 row_idx, row, is_prefill =next(gen)78# Inject user tokens one at a time while displaying each row.9user_tokens = tokenizer.encode(" Hey, what's up?", add_special_tokens=False)10for tok in user_tokens:11 row_idx, row, is_prefill = gen.send(tok)12print(row_idx, row)
Interactive demo (curses UI)
The repo ships a curses-based demo at examples/demo_interactive.py — type
freely while the model keeps generating all ten channels in parallel. Each
keystroke queues a user token; Esc pauses/resumes:
The collator handles row-by-row flattening, the block-causal additive mask,
shift-by-num_channels labels, and padding. Let me know if this actually trains :), good luck.
model.generate() is intentionally disabled
The standard GenerationMixin.generate() would produce gibberish on this
model (no channel ids, no block-causal mask). It raises
NotImplementedError with a pointer to model.stream_generate(...). This
also blocks pipeline("text-generation", ...) which calls generate()
internally.
The full model.forward(input_ids=..., channel_ids=..., attention_mask=...)
path remains available for power users who want custom rollouts — see
stream_inference.generate() for the canonical reference implementation
(also bundled in this repo).
Recommended sampling settings
The numbers in the paper used:
Knob
Value
temperature
0.6
top_p
0.95
top_k
20
silence_penalty
0.0
skip_silence
True
warm_start (sys prompt)
True
max_rows
1024
Lower silence_penalty or disable skip_silence for less aggressive Output
output.
Training
Trained on JonasGeiping/stream-data (see dataset card). Recipe details: