1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! The one place that decides which tokens the repetition, presence
//! and frequency penalties look back over.
//!
//! # What llama.cpp does, and where
//!
//! llama.cpp's penalties sampler is stateful: it keeps a ring buffer of
//! the last `penalty_last_n` tokens it has ACCEPTED, plus a count map
//! over that buffer, and `apply` walks the candidate list looking each
//! candidate up in the map (`src/llama-sampler.cpp:2698-2759`). Nothing
//! about that buffer knows whether a token was generated or read out of
//! the prompt -- only that the sampler was told about it.
//!
//! Both front ends tell it about the prompt.
//!
//! - `llama-server` seeds the sampler with every prompt token before
//! the first token is drawn
//! (`tools/server/server-context.cpp:375-397`, the loop at 386-390:
//! `for (int i = 0; i < prompt.tokens.size(); i++) { ...
//! common_sampler_accept(smpl.get(), id, false); }`).
//! - `llama-cli` does the same as it consumes the prompt, with the
//! reason written on the line above
//! (`tools/completion/completion.cpp:730-736`: *"push the prompt in
//! the sampling context in order to apply repetition penalties
//! later"*, `common_sampler_accept(smpl, embd_inp[n_consumed],
//! /* accept_grammar= */ false)`).
//!
//! `common_sampler_accept` pushes into the chain unconditionally
//! (`common/sampling.cpp:472-504`), so a prompt token lands in the
//! penalties ring buffer exactly like a generated one.
//!
//! So llama.cpp's window is the last `penalty_last_n` tokens of
//! `prompt ++ generated`, and ferrox matches that. **This changes
//! output** relative to ferrox before this module existed, on every run
//! at the default `--repeat-penalty 1.1`: a token that occurs in the
//! prompt is now penalised on its first generated occurrence.
//!
//! # Why it is a type and not a slice
//!
//! Because it was a slice, and five call sites each chose their own.
//! `ferrox run`'s decode loops passed the generated tokens; the server's
//! two decode loops passed the generated tokens (still do -- issue #73,
//! the prompt ids do not reach that seam); `speculative` passed
//! the prompt as well and then grew a `penalty_history_start` knob to
//! paper over the disagreement; `draft_model` cloned the whole history
//! per block; `kimi_generate` passed prompt and generated and was the
//! only one that matched llama.cpp. Five sites, four answers, nothing
//! enforcing agreement -- this repo's dominant bug shape.
//!
//! A [`PenaltyWindow`] is built from BOTH halves and there is no
//! constructor that takes one slice, so a caller cannot produce a window
//! without saying what its prompt is. A caller that genuinely has none
//! writes `&[]` and that is visible in the diff.
/// The tokens the penalties may see: a prompt and the tokens generated
/// after it, in that order.
///
/// Borrowed rather than owned because this is built once per sampled
/// token on every decode loop in the workspace; an owning window would
/// clone the whole sequence per token.
///
/// The two halves are kept separate rather than concatenated because a
/// decode loop already holds them separately, and concatenating would
/// mean an allocation per token for a value only ever read back as "the
/// last N of the two".