Skip to content

Commit ef8b445

Browse files
authored
Merge pull request #61 from interscript/fix/decode-repetition-guard
fix(imf): cut greedy-KV generation when the token window cycles
2 parents ec00f7c + 3c32a77 commit ef8b445

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

‎src/imf/export.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ def onnx_greedy_kv(encoder_sess, kv_sess, text: str, max_len: int = 256) -> list
393393
pasts = _zero_pasts(kv_sess)
394394
cur = np.array([[0]], dtype=np.int64)
395395
generated: list[int] = []
396+
# Flat-byte students can loop on short inputs (greedy KV without
397+
# sampling): cut generation when the recent token window repeats
398+
# verbatim — the live failure was diacritization echoing the same
399+
# phrase until max_len.
400+
window = 24
401+
joined = ""
396402
for _ in range(max_len):
397403
out = kv_sess.run(None, {"input_ids": cur, "encoder_hidden_states": hidden, **pasts})
398404
results = dict(zip(out_names, out, strict=True))
@@ -405,6 +411,13 @@ def onnx_greedy_kv(encoder_sess, kv_sess, text: str, max_len: int = 256) -> list
405411
for name in pasts
406412
}
407413
cur = np.array([[nxt]], dtype=np.int64)
414+
if len(generated) >= 2 * window:
415+
joined += f",{nxt}"
416+
needle = ",".join(str(t) for t in generated[-window:])
417+
if joined.find(needle) < len(joined) - len(needle):
418+
break
419+
else:
420+
joined = ",".join(str(t) for t in generated)
408421
return generated
409422

410423

‎tests/test_decode_guard.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Repetition guard in the greedy KV decode: flat-byte students loop
2+
on short inputs (live: كتاب -> كَتَابٍ: كَتَابٍ: ... until max_len).
3+
The guard must cut generation when the recent token window cycles."""
4+
5+
from src.imf.export import onnx_greedy_kv
6+
7+
8+
class _FakeEncoder:
9+
def run(self, _, feeds):
10+
import numpy as np
11+
12+
return [np.zeros((1, feeds["input_ids"].shape[1], 4), dtype=np.float32)]
13+
14+
15+
class _FakeKV:
16+
"""Cycles two tokens forever, never EOS — the pathological loop."""
17+
18+
def __init__(self):
19+
import numpy as np
20+
21+
self.np = np
22+
self.step = 0
23+
24+
def get_outputs(self):
25+
class O:
26+
def __init__(self, name):
27+
self.name = name
28+
29+
return [O("logits"), O("present_k"), O("present_v")]
30+
31+
def get_inputs(self):
32+
class I:
33+
def __init__(self, name, shape, typ="tensor(float)"):
34+
self.name = name
35+
self.shape = shape
36+
self.type = typ
37+
38+
return [
39+
I("input_ids", [1, "seq"]),
40+
I("encoder_hidden_states", [1, "seq", 4]),
41+
I("past_k", [1, 4, "past", 8]),
42+
I("past_v", [1, 4, "past", 8]),
43+
]
44+
45+
def run(self, _, feeds):
46+
import numpy as np
47+
48+
logits = np.full((1, 1, 260), -1e9)
49+
# visible cycle: token 10, 11, 10, 11, ...
50+
logits[0, -1, 10 if self.step % 2 == 0 else 11] = 1e9
51+
self.step += 1
52+
return [logits, np.zeros((1,)), np.zeros((1,))]
53+
54+
55+
def test_looping_model_is_cut_before_max_len():
56+
out = onnx_greedy_kv(_FakeEncoder(), _FakeKV(), "x", max_len=4096)
57+
# 2-token cycle caught by the window guard well before max_len
58+
assert len(out) < 200
59+
assert out[:4] == [10, 11, 10, 11]
60+
61+
62+
def test_normal_generation_unaffected():
63+
class _StopKV(_FakeKV):
64+
def run(self, _, feeds):
65+
import numpy as np
66+
67+
logits = np.full((1, 1, 260), -1e9)
68+
if self.step >= 5:
69+
logits[0, -1, 1] = 1e9 # EOS_ID
70+
else:
71+
logits[0, -1, 20 + self.step] = 1e9
72+
self.step += 1
73+
return [logits, np.zeros((1,)), np.zeros((1,))]
74+
75+
out = onnx_greedy_kv(_FakeEncoder(), _StopKV(), "x", max_len=256)
76+
assert out == [20, 21, 22, 23, 24]

0 commit comments

Comments
 (0)