generate() vs stream_generate()
The two-liner from lesson 1 used generate(), which gives you the full completion as one string when it's done. For UI work — chat bubbles, terminal streaming, anything where you want tokens to appear as they're produced — you want stream_generate(). It's a Python generator that yields one chunk per token.
Both are real APIs in mlx-lm. generate() is implemented as a thin wrapper around stream_generate() that joins the chunks. Use the streaming version when latency matters and the buffering version when it doesn't.
What stream_generate yields
Each yield is a GenerationResponse object with rich metadata, not just a raw token. The fields you'll actually use are:
.text— the new text fragment for this step (often a whole token, sometimes part of a Unicode glyph being streamed in pieces)..finish_reason—Nonewhile still generating, then'stop'(hit a stop token) or'length'(hit max_tokens) on the last yield..generation_tokens— running count of tokens generated so far..generation_tps— measured tokens-per-second since generation started; useful for live throughput display..peak_memory— peak GPU memory in bytes during this generation.
The richness of this metadata is one of mlx-lm's quiet strengths — you don't need a separate profiler to see throughput, and you don't have to guess why generation stopped.
Stop conditions, demystified
Generation stops when one of three things happens:
- The model emits the EOS token for its tokenizer (e.g.
<|eot_id|>for Llama 3,<|im_end|>for Qwen). The tokenizer knows its own EOS; mlx-lm picks it up from there.finish_reason='stop'. - You hit
max_tokens.finish_reason='length'. The model would have kept going if you'd let it. - You manually stop by breaking out of the generator. The model is mid-sentence, no special finish_reason — just no more yields.
The trap most quickstarts don't mention: chat-tuned models depend on the EOS token to stop on every turn. If you forget to apply the chat template (lesson 5), the model often won't emit its EOS at the natural stopping point, and you'll see runaway generation that only stops when max_tokens kicks in.