-
Notifications
You must be signed in to change notification settings - Fork 0
feat(spec): GPU draft sampled speculative decoding for Gemma 4 (#178) #295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,6 +99,103 @@ public static int Sample(ReadOnlySpan<float> logits, SamplingParams p, Random? r | |
| return SampleFromDistribution(probs, rng); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Build the full-vocabulary, post-filter sampling distribution for <paramref name="logits"/> | ||
| /// under <paramref name="p"/> into <paramref name="probs"/> (length must equal | ||
| /// <c>logits.Length</c>; entries outside the kept support are 0 and the kept entries sum to 1). | ||
| /// Applies the SAME pipeline as <see cref="Sample"/>'s slow path — logit bias, repetition | ||
| /// penalty, temperature, softmax, top-k (+ renormalise), min-p, top-p, normalise — so a token | ||
| /// drawn from this distribution is distributed identically to <see cref="Sample"/>. | ||
| /// | ||
| /// Used by distribution-preserving speculative sampling (issue #178), which needs the draft | ||
| /// proposal probability q(x) and the residual max(0, p − q) over a SHARED support: the draft | ||
| /// and target distributions MUST be built with the same <paramref name="p"/> for the | ||
| /// min(1, p/q) accept ratio to be meaningful. On Temperature ≤ 0 writes a one-hot at the | ||
| /// greedy argmax. | ||
| /// </summary> | ||
| public static void BuildFilteredDistribution(ReadOnlySpan<float> logits, SamplingParams p, Span<float> probs) | ||
| { | ||
| int vocabSize = logits.Length; | ||
| if (probs.Length != vocabSize) | ||
| throw new ArgumentException( | ||
| $"probs length ({probs.Length}) must equal logits length ({vocabSize}).", nameof(probs)); | ||
|
|
||
| if (p.Temperature <= 0f) | ||
| { | ||
| probs.Clear(); | ||
| probs[Greedy(logits)] = 1f; | ||
| return; | ||
| } | ||
|
|
||
| logits.CopyTo(probs); | ||
|
|
||
| // Logit bias (additive, before temperature) — mirrors Sample's slow path. | ||
| if (p.LogitBias is { Count: > 0 }) | ||
| foreach (var (id, bias) in p.LogitBias) | ||
| if ((uint)id < (uint)vocabSize) | ||
| probs[id] += bias; | ||
|
|
||
| // Repetition penalty (in logit space, before temperature). | ||
| if (p.RepetitionPenalty != 1.0f && p.PreviousTokens is { Count: > 0 }) | ||
| foreach (int id in p.PreviousTokens) | ||
| if ((uint)id < (uint)vocabSize) | ||
| probs[id] = probs[id] > 0f ? probs[id] / p.RepetitionPenalty : probs[id] * p.RepetitionPenalty; | ||
|
|
||
| if (p.Temperature != 1.0f) | ||
| { | ||
| float invTemp = 1.0f / p.Temperature; | ||
| for (int i = 0; i < vocabSize; i++) | ||
| probs[i] *= invTemp; | ||
| } | ||
|
|
||
| Softmax(probs); | ||
|
|
||
| if (p.TopK > 0 && p.TopK < vocabSize) | ||
| { | ||
| ApplyTopK(probs, p.TopK); | ||
| Normalize(probs); | ||
| } | ||
| if (p.MinP > 0f) | ||
| ApplyMinP(probs, p.MinP); | ||
| if (p.TopP < 1.0f && p.TopP > 0f) | ||
| ApplyTopP(probs, p.TopP); | ||
| Normalize(probs); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sample like <see cref="Sample"/> but also materialise the full post-filter distribution | ||
| /// into <paramref name="probs"/> (see <see cref="BuildFilteredDistribution"/>) and return the | ||
| /// drawn token, so <c>probs[token]</c> is its proposal probability q(token). On Temperature ≤ 0 | ||
| /// returns the greedy argmax. Distribution-preserving speculative sampling (issue #178) uses | ||
| /// this to propose draft tokens while retaining q for the accept/residual test. | ||
| /// </summary> | ||
| public static int SampleWithDistribution(ReadOnlySpan<float> logits, SamplingParams p, Span<float> probs, Random? rng = null) | ||
| { | ||
| if (p.Temperature <= 0f) | ||
| { | ||
| // One-hot at the greedy argmax — no filter pipeline, no RNG draw. | ||
| int argmax = Greedy(logits); | ||
| probs.Clear(); | ||
| probs[argmax] = 1f; | ||
| return argmax; | ||
| } | ||
| BuildFilteredDistribution(logits, p, probs); | ||
| rng ??= Random.Shared; | ||
| return SampleFromDistribution(probs, rng); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Draw a token from a pre-computed normalized distribution (e.g. one already built by | ||
| /// <see cref="BuildFilteredDistribution"/>), avoiding a redundant rebuild. Used by the | ||
| /// speculative decoder's looser-accept reject branch, where the target distribution at the | ||
| /// rejection position is already in hand. | ||
| /// </summary> | ||
| public static int SampleFromProbs(ReadOnlySpan<float> probs, Random rng) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(rng); | ||
| return SampleFromDistribution(probs, rng); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Top-k-first sampling fast path (no logit bias). Selects the top-k logits in one | ||
| /// O(vocab) pass, then applies temperature, softmax, min-p, and top-p over only those | ||
|
|
@@ -367,6 +464,48 @@ private static int SampleFromDistribution(ReadOnlySpan<float> probs, Random rng) | |
| return probs.Length - 1; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sample a token proportional to the residual max(0, p[x] − q[x]) of two full-vocabulary | ||
| /// distributions built with the SAME <see cref="SamplingParams"/> (see | ||
| /// <see cref="BuildFilteredDistribution"/>). This is the rejection correction of speculative | ||
| /// sampling (Leviathan et al. / Chen et al.): drawing the corrected token from the residual | ||
| /// after a draft proposal is rejected makes the overall emitted-token distribution identical | ||
| /// to sampling directly from <paramref name="p"/>. If the residual mass is ≈0 (numerical / | ||
| /// q already dominated p on the kept support), falls back to sampling from <paramref name="p"/>. | ||
| /// </summary> | ||
| public static int ResampleResidual(ReadOnlySpan<float> p, ReadOnlySpan<float> q, Random rng) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(rng); | ||
| if (p.Length != q.Length) | ||
| throw new ArgumentException( | ||
| $"p and q must have equal length ({p.Length} vs {q.Length}).", nameof(q)); | ||
|
|
||
| float sum = 0f; | ||
| for (int i = 0; i < p.Length; i++) | ||
| { | ||
| float r = p[i] - q[i]; | ||
| if (r > 0f) sum += r; | ||
| } | ||
| if (sum <= 0f || float.IsNaN(sum)) | ||
| return SampleFromDistribution(p, rng); // degenerate: empty residual → fall back to p | ||
|
|
||
| float target = (float)rng.NextDouble() * sum; | ||
| float cum = 0f; | ||
| int lastPositive = -1; | ||
| for (int i = 0; i < p.Length; i++) | ||
| { | ||
| float r = p[i] - q[i]; | ||
| if (r > 0f) | ||
| { | ||
| lastPositive = i; | ||
| cum += r; | ||
| if (target <= cum) return i; | ||
| } | ||
| } | ||
| // Rounding fallback: the last token with positive residual (tracked above, no extra pass). | ||
| return lastPositive >= 0 ? lastPositive : SampleFromDistribution(p, rng); | ||
| } | ||
|
Comment on lines
+492
to
+507
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In float target = (float)rng.NextDouble() * sum;
float cum = 0f;
int lastPositiveIdx = -1;
for (int i = 0; i < p.Length; i++)
{
float r = p[i] - q[i];
if (r > 0f)
{
lastPositiveIdx = i;
cum += r;
if (target <= cum) return i;
}
}
if (lastPositiveIdx >= 0) return lastPositiveIdx;
return SampleFromDistribution(p, rng); |
||
|
|
||
| /// <summary> | ||
| /// Find the k-th largest value in the span (1-indexed: k=1 is the largest). | ||
| /// Simple O(n*k) selection — fine for Phase 1 correctness. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
SampleWithDistribution, whenp.Temperature <= 0f, we can avoid callingBuildFilteredDistributionentirely. This avoids a redundant call toGreedy(logits)and the overhead of copying/clearing arrays. We also introduce a publicSampleFromProbshelper to allow sampling from a pre-computed distribution directly.