eclipse-senpai commited on
Commit
fce6c24
·
1 Parent(s): 6bbbc02

Fix streaming display bug + deep-blue Minima Labs UI

Browse files

- run_generate double-accumulated cumulative yields (t1+t1t2+t1t2t3);
generate() now yields per-token chunks, accumulated once
- effort is a real gr.Radio input (API/MCP-settable, no gr.State)
- restyle to Minima Labs deep blue; hide Gradio chrome; drop MCP flag

Files changed (2) hide show
  1. app.py +104 -144
  2. loader.py +6 -7
app.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  A 5.76M-parameter looped-hybrid language model. The signature mechanic: one
4
  weight-shared body block runs `K` times per token (the "effort"). More passes
5
- sharpen grammar; fewer favor commonsense. The Space makes the loop visible.
6
 
7
  Runs on ZeroGPU (free for the creator; visitors consume their own quota). The
8
  model is tiny so cold-start weight streaming is near-instant.
@@ -24,54 +24,10 @@ TOKENIZER = load_tokenizer()
24
  _PARAMS = sum(p.numel() for p in MODEL.parameters())
25
  print(f" {round(_PARAMS/1e6, 2)}M params ready on {DEVICE}")
26
 
27
- EFFORTS = [
28
- {"id": "low", "k": 2, "name": "Low", "tag": "commonsense"},
29
- {"id": "med", "k": 3, "name": "Medium", "tag": "grammar", "default": True},
30
- {"id": "high", "k": 4, "name": "High", "tag": "deeper grammar"},
31
- ]
32
- EFFORT_K = {e["id"]: e["k"] for e in EFFORTS}
33
- _MAX_K = max(e["k"] for e in EFFORTS)
34
-
35
-
36
- # ── the effort rail (signature) ─────────────────────────────────────────────
37
- def effort_html(selected_id: str = "med") -> str:
38
- cards = []
39
- for e in EFFORTS:
40
- sel = "selected" if e["id"] == selected_id else ""
41
- dots = "".join(
42
- f'<span class="dot {"lit" if i < e["k"] else ""}"></span>'
43
- for i in range(_MAX_K)
44
- )
45
- cards.append(f"""
46
- <button class="effort-card {sel}" data-id="{e['id']}" type="button">
47
- <span class="eff-label">{e['name']}</span>
48
- <span class="eff-k">K={e['k']}</span>
49
- <span class="eff-dots">{dots}</span>
50
- <span class="eff-tag">{e['tag']}</span>
51
- </button>""")
52
- return f'<div id="effort-rail" max-k="{_MAX_K}">{"".join(cards)}</div>'
53
-
54
-
55
- # On load: attach click listeners to the cards; track selection in window.__effort.
56
- ATTACH_JS = """
57
- () => {
58
- window.__effort = "med";
59
- const rail = document.getElementById('effort-rail');
60
- if (!rail) return;
61
- rail.querySelectorAll('.effort-card').forEach(btn => {
62
- btn.addEventListener('click', () => {
63
- rail.querySelectorAll('.effort-card').forEach(b => b.classList.remove('selected'));
64
- btn.classList.add('selected');
65
- window.__effort = btn.dataset.id;
66
- });
67
- });
68
- }
69
- """
70
-
71
- # Generate-click preamble: override the effort slot with the JS-tracked selection.
72
- READ_EFFORT_JS = """
73
- (prompt, eff, mx, t, k) => [prompt, window.__effort || eff || "med", mx, t, k]
74
- """
75
 
76
 
77
  def _estimate_duration(prompt, effort, max_new, temperature, top_k):
@@ -81,111 +37,117 @@ def _estimate_duration(prompt, effort, max_new, temperature, top_k):
81
 
82
 
83
  @spaces.GPU(duration=_estimate_duration)
84
- def run_generate(prompt: str, effort: str, max_new: int,
85
  temperature: float, top_k: int):
86
- """Generate text from a prompt. `effort` sets the loop count K (2/3/4):
87
- more passes sharpen grammar, fewer favor commonsense. Streams token-by-token."""
88
- picked = EFFORT_K.get(effort or "med", 3)
89
  if not str(prompt).strip():
90
- yield ("<div class='outbox' id='specimen'></div>"
91
- "<div class='status'><span class='empty'>Type a prompt to start.</span></div>")
92
  return
93
- outs = []
94
- last_count = 0
95
- last_tps = 0.0
96
- for text, count, tps in generate(
97
- MODEL, TOKENIZER, prompt, loops=picked, max_new=int(max_new),
98
  temperature=float(temperature), top_k=int(top_k), device=DEVICE):
99
- outs.append(text)
100
- last_tps, last_count = tps, count
101
- status = (f"<div class='status'>"
102
- f"<span class='k-chip'>K={picked}</span>"
103
- f"<span class='stat'>{last_count} tok</span>"
104
- f"<span class='stat'>{last_tps:.1f} tok/s</span>"
105
- f"<span class='stat'>ZeroGPU</span></div>")
106
- yield f"<div class='outbox' id='specimen'>{''.join(outs)}</div>", status
107
- status = (f"<div class='status'>"
108
- f"<span class='k-chip'>K={picked}</span>"
109
- f"<span class='stat'>{last_count} tok</span>"
110
- f"<span class='stat'>{last_tps:.1f} tok/s</span>"
111
- f"<span class='done'>done</span></div>")
112
- yield f"<div class='outbox' id='specimen'>{''.join(outs)}</div>", status
113
 
114
 
115
  CSS = """
116
- @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
117
 
118
  :root{
119
- --bg:#13131A; --surface:#1C1C26; --surface2:#22222E;
120
- --hair:#2A2A38; --text:#E8E6DF; --muted:#8A8A98;
121
- --amber:#E8A547; --amber-dim:#8a6a2e; --teal:#4FB3A0;
122
  --r:10px;
123
  }
124
  *{box-sizing:border-box;}
125
- body, .gradio-container, .gradio-container * { background:var(--bg) !important; color:var(--text) !important; font-family:'Inter',system-ui,sans-serif !important; }
126
- .gradio-container{ max-width:980px !important; padding:28px 20px 48px !important; }
127
-
128
- .head{ display:flex; align-items:baseline; gap:16px; padding-bottom:6px; flex-wrap:wrap; }
129
- .breed{ font-family:'Fraunces',serif; font-weight:300; font-size:30px; line-height:1; letter-spacing:-0.01em; color:var(--text); }
130
- .breed em{ font-style:italic; font-weight:500; color:var(--amber); }
131
- .kicker{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--muted); border-bottom:1px solid var(--hair); padding-bottom:8px; width:100%; margin-top:4px; }
132
- .lede{ font-family:'Fraunces',serif; font-weight:300; font-size:15.5px; line-height:1.5; color:#b7b6ad; max-width:62ch; margin:14px 0 4px; }
133
- .lede b{ font-weight:500; color:var(--teal); }
134
-
135
- .panel-h{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted); margin:22px 0 10px; }
136
- #effort-rail{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }
137
- .effort-card{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r); padding:12px 14px 12px; cursor:pointer; display:grid; grid-template-columns:auto 1fr auto; grid-template-rows:auto auto; align-items:center; gap:2px 10px; transition:border-color .15s, background .15s; text-align:left; }
138
- .effort-card:hover{ border-color:#3a3a4a; }
139
- .effort-card.selected{ background:var(--surface2); border-color:var(--amber); box-shadow:0 0 0 1px var(--amber) inset; }
140
- .eff-label{ font-family:'Inter'; font-weight:600; font-size:14px; color:var(--text); }
141
- .effort-card.selected .eff-label{ color:var(--amber); }
142
- .eff-k{ grid-column:2; font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--muted); }
143
- .effort-card.selected .eff-k{ color:#c9a05a; }
144
- .eff-dots{ grid-column:1 / span 3; grid-row:2; display:flex; gap:4px; margin-top:6px; }
145
- .dot{ width:7px; height:7px; border-radius:50%; background:#33333f; transition:background .15s, box-shadow .15s; }
146
- .effort-card .dot.lit{ background:#4a4a3a; }
147
- .effort-card.selected .dot.lit{ background:var(--amber); box-shadow:0 0 5px rgba(232,165,71,.55); }
148
- .eff-tag{ grid-column:3; grid-row:1; font-family:'Inter'; font-size:10.5px; color:#6f6f7d; text-transform:lowercase; }
149
- .effort-card.selected .eff-tag{ color:var(--amber-dim); }
150
-
151
- textarea#prompt, .gradio-container textarea{ background:var(--surface) !important; border:1px solid var(--hair) !important; border-radius:var(--r) !important; color:var(--text) !important; font-family:'Fraunces',serif !important; font-weight:300 !important; font-size:16px !important; line-height:1.5 !important; }
152
- textarea#prompt:focus{ border-color:var(--amber) !important; }
153
- .label-wrap label, .gradio-container .form label{ color:#b7b6ad !important; font-size:11.5px !important; letter-spacing:0.04em !important; }
154
-
155
- button#gen, button.primary{ background:var(--amber) !important; color:#1a1409 !important; border:none !important; border-radius:var(--r) !important; font-weight:600 !important; font-size:14px !important; }
156
- button#gen:hover{ filter:brightness(1.08); }
157
-
158
- .outbox{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r); padding:18px 20px; min-height:140px; font-family:'Fraunces',serif; font-weight:300; font-size:17px; line-height:1.55; color:var(--text); white-space:pre-wrap; word-break:break-word; }
159
- .empty{ color:#55556a; font-style:italic; font-family:'Fraunces',serif; }
160
-
161
- .status{ font-family:'IBM Plex Mono',monospace; font-size:11.5px; display:flex; gap:12px; align-items:center; margin-top:10px; flex-wrap:wrap; }
162
- .stat{ color:var(--muted); }
163
- .k-chip{ background:rgba(232,165,71,.12); color:var(--amber); border:1px solid rgba(232,165,71,.35); border-radius:5px; padding:1px 7px; font-weight:500; }
164
- .done{ color:var(--teal); }
165
-
166
- input[type=range]{ accent-color:var(--amber); }
167
-
168
- .foot{ margin-top:28px; border-top:1px solid var(--hair); padding-top:12px; font-family:'IBM Plex Mono',monospace; font-size:10.5px; color:#5d5d6c; display:flex; justify-content:space-between; flex-wrap:wrap; gap:8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  .foot a{ color:var(--muted); text-decoration:none; border-bottom:1px dotted var(--hair); }
170
  .foot a:hover{ color:var(--text); }
171
  """
172
 
173
  HEADER_HTML = """
174
  <div class="head">
175
- <div class="breed">min&nbsp;·spark <em>preview</em></div>
176
- <div class="kicker">PICO release 01 · Meiosis · looped hybrid · 5.76M params · ZeroGPU</div>
177
- </div>
178
- <div class="lede">
179
- A research demo of a small language model trained from scratch on ten billion tokens.
180
- Its one mechanic: a single weight-shared block runs <b>K times per token</b> — that is the
181
- &ldquo;effort.&rdquo; More passes sharpen grammar; fewer favor commonsense. Pick a pass count
182
- and prompt it.
183
  </div>
184
  """
185
 
186
  FOOTER_HTML = """
187
  <div class="foot">
188
- <span>ZeroGPU · generations stop early on &lt;eos&gt; · visitors use their own quota</span>
189
  <span><a href="https://github.com/eclipse-senpai/PICO" target="_blank" rel="noopener">PICO on GitHub</a></span>
190
  </div>
191
  """
@@ -193,34 +155,32 @@ FOOTER_HTML = """
193
  with gr.Blocks(css=CSS, title="min-spark · Meiosis preview") as demo:
194
  gr.HTML(HEADER_HTML)
195
 
196
- gr.HTML('<div class="panel-h">Effort — loops per token</div>')
197
- effort_view = gr.HTML(effort_html("med"))
 
198
 
199
- gr.HTML('<div class="panel-h">Prompt</div>')
200
- prompt = gr.Textbox(value="", placeholder="Once upon a time, the very small model",
201
- elem_id="prompt", lines=2, show_label=False)
202
 
203
  with gr.Row():
204
  max_new = gr.Slider(8, 256, value=128, step=8, label="Max tokens")
205
  temperature = gr.Slider(0.1, 1.6, value=0.8, step=0.05, label="Temperature")
206
- top_k = gr.Slider(0, 200, value=50, step=5, label="Top-k (0 = off)")
207
 
208
  gen_btn = gr.Button("Generate", elem_id="gen", variant="primary")
209
 
210
- out = gr.HTML(value='<div class="outbox" id="specimen"></div>')
 
211
  status = gr.HTML(value='<div class="status"></div>')
212
 
213
  gr.HTML(FOOTER_HTML)
214
 
215
- effort_state = gr.State("med") # default; overridden by READ_EFFORT_JS preamble
216
-
217
- demo.load(fn=None, js=ATTACH_JS)
218
  gen_btn.click(
219
  fn=run_generate,
220
- js=READ_EFFORT_JS,
221
- inputs=[prompt, effort_state, max_new, temperature, top_k],
222
  outputs=[out, status],
223
  )
224
 
225
  if __name__ == "__main__":
226
- demo.queue().launch(mcp_server=True)
 
2
 
3
  A 5.76M-parameter looped-hybrid language model. The signature mechanic: one
4
  weight-shared body block runs `K` times per token (the "effort"). More passes
5
+ sharpen grammar; fewer favor commonsense.
6
 
7
  Runs on ZeroGPU (free for the creator; visitors consume their own quota). The
8
  model is tiny so cold-start weight streaming is near-instant.
 
24
  _PARAMS = sum(p.numel() for p in MODEL.parameters())
25
  print(f" {round(_PARAMS/1e6, 2)}M params ready on {DEVICE}")
26
 
27
+ # effort label → loop count. The Radio VALUES are what the API/MCP sees.
28
+ EFFORT_CHOICES = [("Low · K=2 commonsense", 2),
29
+ ("Medium · K=3 grammar", 3),
30
+ ("High · K=4 deeper grammar", 4)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
 
33
  def _estimate_duration(prompt, effort, max_new, temperature, top_k):
 
37
 
38
 
39
  @spaces.GPU(duration=_estimate_duration)
40
+ def run_generate(prompt: str, effort: int, max_new: int,
41
  temperature: float, top_k: int):
42
+ """Generate text from a prompt. `effort` is the loop count K (2/3/4): more
43
+ passes sharpen grammar, fewer favor commonsense. Streams token-by-token."""
44
+ k = int(effort) if effort in (2, 3, 4) else 3
45
  if not str(prompt).strip():
46
+ yield "", "<div class='status'>Type a prompt to start.</div>"
 
47
  return
48
+ text = ""
49
+ count = 0
50
+ tps = 0.0
51
+ for chunk, count, tps in generate(
52
+ MODEL, TOKENIZER, prompt, loops=k, max_new=int(max_new),
53
  temperature=float(temperature), top_k=int(top_k), device=DEVICE):
54
+ text += chunk
55
+ yield text, f"<div class='status'>K={k} · {count} tok · {tps:.1f} tok/s</div>"
56
+ yield text, f"<div class='status'>K={k} · {count} tok · {tps:.1f} tok/s · done</div>"
 
 
 
 
 
 
 
 
 
 
 
57
 
58
 
59
  CSS = """
60
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
61
 
62
  :root{
63
+ --bg:#0A1128; --surface:#101A3A; --surface2:#16224A;
64
+ --hair:#22315E; --text:#E8EDF7; --muted:#7D8DB0;
65
+ --blue:#3E7BFA; --blue-bright:#5E96FF; --blue-dim:#2A4A8F;
66
  --r:10px;
67
  }
68
  *{box-sizing:border-box;}
69
+ body, .gradio-container{ background:var(--bg) !important; }
70
+ .gradio-container{
71
+ max-width:820px !important; margin:0 auto !important;
72
+ padding:32px 20px 40px !important;
73
+ font-family:'Inter',system-ui,sans-serif !important; color:var(--text) !important;
74
+ }
75
+
76
+ /* hide Gradio chrome: footer, settings, API/MCP links */
77
+ footer, .gradio-container footer, #footer{ display:none !important; }
78
+
79
+ .head{ margin-bottom:20px; }
80
+ .title{ font-weight:600; font-size:26px; letter-spacing:-0.01em; color:var(--text); }
81
+ .title em{ font-style:normal; color:var(--blue-bright); }
82
+ .sub{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.16em;
83
+ text-transform:uppercase; color:var(--muted); margin-top:6px; }
84
+
85
+ .field-h{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.16em;
86
+ text-transform:uppercase; color:var(--muted); margin:18px 0 8px; }
87
+
88
+ /* prompt */
89
+ .gradio-container textarea{
90
+ background:var(--surface) !important; border:1px solid var(--hair) !important;
91
+ border-radius:var(--r) !important; color:var(--text) !important;
92
+ font-family:'Inter' !important; font-size:15px !important; line-height:1.5 !important;
93
+ }
94
+ .gradio-container textarea:focus{ border-color:var(--blue) !important; box-shadow:none !important; }
95
+
96
+ /* effort radio — pill row */
97
+ #effort .wrap{ display:flex !important; gap:8px !important; }
98
+ #effort label{
99
+ flex:1; background:var(--surface) !important; border:1px solid var(--hair) !important;
100
+ border-radius:var(--r) !important; padding:10px 12px !important; cursor:pointer;
101
+ font-size:13px !important; color:var(--muted) !important; text-align:center;
102
+ transition:border-color .15s, background .15s, color .15s;
103
+ }
104
+ #effort label:hover{ border-color:#33508f !important; }
105
+ #effort label.selected{
106
+ background:var(--surface2) !important; border-color:var(--blue) !important;
107
+ color:var(--blue-bright) !important; box-shadow:0 0 0 1px var(--blue) inset !important;
108
+ }
109
+ #effort input{ display:none !important; }
110
+
111
+ /* sliders */
112
+ .gradio-container .form label, .label-wrap label{
113
+ color:var(--muted) !important; font-size:12px !important; letter-spacing:0.02em !important;
114
+ }
115
+ input[type=range]{ accent-color:var(--blue); }
116
+
117
+ /* generate button */
118
+ button#gen{
119
+ background:var(--blue) !important; color:#fff !important; border:none !important;
120
+ border-radius:var(--r) !important; font-weight:600 !important; font-size:14px !important;
121
+ margin-top:6px;
122
+ }
123
+ button#gen:hover{ background:var(--blue-bright) !important; }
124
+
125
+ /* output */
126
+ #out textarea, #out{
127
+ background:var(--surface) !important; border:1px solid var(--hair) !important;
128
+ border-radius:var(--r) !important; color:var(--text) !important;
129
+ font-size:15.5px !important; line-height:1.6 !important;
130
+ }
131
+ .status{ font-family:'IBM Plex Mono',monospace; font-size:11.5px; color:var(--muted);
132
+ margin-top:8px; min-height:16px; }
133
+
134
+ .foot{ margin-top:26px; border-top:1px solid var(--hair); padding-top:12px;
135
+ font-family:'IBM Plex Mono',monospace; font-size:10.5px; color:#54648c;
136
+ display:flex; justify-content:space-between; flex-wrap:wrap; gap:8px; }
137
  .foot a{ color:var(--muted); text-decoration:none; border-bottom:1px dotted var(--hair); }
138
  .foot a:hover{ color:var(--text); }
139
  """
140
 
141
  HEADER_HTML = """
142
  <div class="head">
143
+ <div class="title">min-spark <em>preview</em></div>
144
+ <div class="sub">Minima Labs · PICO release 01 · Meiosis · looped hybrid · 5.76M params</div>
 
 
 
 
 
 
145
  </div>
146
  """
147
 
148
  FOOTER_HTML = """
149
  <div class="foot">
150
+ <span>ZeroGPU · stops early on &lt;eos&gt; · visitors use their own quota</span>
151
  <span><a href="https://github.com/eclipse-senpai/PICO" target="_blank" rel="noopener">PICO on GitHub</a></span>
152
  </div>
153
  """
 
155
  with gr.Blocks(css=CSS, title="min-spark · Meiosis preview") as demo:
156
  gr.HTML(HEADER_HTML)
157
 
158
+ gr.HTML('<div class="field-h">Effort — loops per token</div>')
159
+ effort = gr.Radio(choices=EFFORT_CHOICES, value=3, elem_id="effort",
160
+ show_label=False, container=False)
161
 
162
+ gr.HTML('<div class="field-h">Prompt</div>')
163
+ prompt = gr.Textbox(value="", placeholder="Once upon a time",
164
+ lines=2, show_label=False, container=False)
165
 
166
  with gr.Row():
167
  max_new = gr.Slider(8, 256, value=128, step=8, label="Max tokens")
168
  temperature = gr.Slider(0.1, 1.6, value=0.8, step=0.05, label="Temperature")
169
+ top_k = gr.Slider(0, 200, value=50, step=5, label="Top-k (0 = off)")
170
 
171
  gen_btn = gr.Button("Generate", elem_id="gen", variant="primary")
172
 
173
+ out = gr.Textbox(value="", elem_id="out", show_label=False, container=False,
174
+ lines=10, interactive=False, autoscroll=True)
175
  status = gr.HTML(value='<div class="status"></div>')
176
 
177
  gr.HTML(FOOTER_HTML)
178
 
 
 
 
179
  gen_btn.click(
180
  fn=run_generate,
181
+ inputs=[prompt, effort, max_new, temperature, top_k],
 
182
  outputs=[out, status],
183
  )
184
 
185
  if __name__ == "__main__":
186
+ demo.queue().launch()
loader.py CHANGED
@@ -44,13 +44,13 @@ def load_model(device: str = "cpu") -> Meiosis:
44
  @torch.no_grad()
45
  def generate(model, tokenizer, prompt: str, *, loops: int, max_new: int,
46
  temperature: float, top_k: int, device: str):
47
- """Token-by-token sampling. Yields (text_so_far, token_count, tok_per_s).
48
- Mirrors infer.py: EOS prefix, top-k + temperature, stop on EOS. Runs on the
49
- GPU worker when invoked under @spaces.GPU; returns only CPU-safe Python
50
- objects (str/int/float) so nothing CUDA crosses the pickle boundary."""
 
51
  import time
52
  ids = [EOS_ID] + tokenizer.encode(prompt).ids
53
- out_text = ""
54
  t0 = None
55
  count = 0
56
  for _ in range(max_new):
@@ -68,7 +68,6 @@ def generate(model, tokenizer, prompt: str, *, loops: int, max_new: int,
68
  if next_id == EOS_ID:
69
  break
70
  ids.append(next_id)
71
- out_text += tokenizer.decode([next_id])
72
  count += 1
73
  elapsed = time.perf_counter() - t0
74
- yield out_text, count, (count / elapsed if elapsed > 0 else 0.0)
 
44
  @torch.no_grad()
45
  def generate(model, tokenizer, prompt: str, *, loops: int, max_new: int,
46
  temperature: float, top_k: int, device: str):
47
+ """Token-by-token sampling, mirroring infer.py. Yields (chunk, count, tps)
48
+ where `chunk` is THIS step's decoded token (not cumulative) so the caller
49
+ can stream a typewriter effect. Per-token decode joins byte-exactly for
50
+ this byte-level BPE (verified against cumulative decode). Runs on the GPU
51
+ worker under @spaces.GPU; yields only CPU-safe Python objects."""
52
  import time
53
  ids = [EOS_ID] + tokenizer.encode(prompt).ids
 
54
  t0 = None
55
  count = 0
56
  for _ in range(max_new):
 
68
  if next_id == EOS_ID:
69
  break
70
  ids.append(next_id)
 
71
  count += 1
72
  elapsed = time.perf_counter() - t0
73
+ yield tokenizer.decode([next_id]), count, (count / elapsed if elapsed > 0 else 0.0)