Skip to content

Commit 4e90bc3

Browse files
committed
Two ops for masked-diffusion token decoding (OmniVoice's per-step select): masked_diffusion_scores fuses the classifier-free-guidance log-softmax over a (rows, C*V) logit block, the MASK-id exclusion, the argmax or top-fraction Gumbel sample, and the per-position confidence minus a per-codebook layer penalty plus position-temperature Gumbel noise, writing a (C, T) prediction grid and a score grid that is -inf wherever the token grid is already unmasked; masked_diffusion_commit writes the k selected flat positions from the prediction into the token grid and stamps the step index into an unmask-step grid; the uniforms come from a splitmix64 counter hash over (seed, domain, index) in detail/hash_rng.h so CPU and CUDA draw bit-identical noise and the CUDA kernel avoids FMA contraction so scores match CPU to ~3e-6 with identical selections; top_k_rows loses its CUDA limit of k <= 6144 (the per-thread shared-memory staging) and Metal's k <= 256 through a rank-selection kernel above k = 256 and a partial_sort on CPU above k = 64, all keeping the descending-value lower-index-first order, verified on one row of 32768 with ties at k from 1 to the full row; tests/test_masked_diffusion.cpp checks the small cases against an in-test double-precision reference and CPU against CUDA at C=8 T=400 V=1025
1 parent a9b09c2 commit 4e90bc3

16 files changed

Lines changed: 1892 additions & 8 deletions

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ add_library(brotensor_cpu STATIC
185185
src/cpu/resample1d.cpp
186186
src/cpu/log_exp_round.cpp
187187
src/cpu/sample_logits.cpp
188+
src/cpu/masked_diffusion.cpp
188189
src/cpu/noise.cpp
189190
src/cpu/l2_norm.cpp
190191
src/cpu/gated_delta_rule.cpp
@@ -284,6 +285,7 @@ if(BROTENSOR_WITH_CUDA)
284285
src/cuda/codec_quant.cu
285286
src/cuda/resample1d.cu
286287
src/cuda/sample_logits.cu
288+
src/cuda/masked_diffusion.cu
287289
src/cuda/noise.cu
288290
src/cuda/l2_norm.cu
289291
src/cuda/gated_delta_rule.cu
@@ -382,6 +384,7 @@ if(BROTENSOR_WITH_METAL)
382384
src/metal/resample1d.mm
383385
src/metal/log_exp_round.mm
384386
src/metal/sample_logits.mm
387+
src/metal/masked_diffusion.mm
385388
src/metal/noise.mm
386389
src/metal/l2_norm.mm
387390
src/metal/l2_normalize.mm
@@ -464,6 +467,7 @@ if(BROTENSOR_WITH_METAL)
464467
src/metal/resample1d.mm
465468
src/metal/log_exp_round.mm
466469
src/metal/sample_logits.mm
470+
src/metal/masked_diffusion.mm
467471
src/metal/noise.mm
468472
src/metal/l2_norm.mm
469473
src/metal/l2_normalize.mm
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#pragma once
2+
3+
// brotensor detail/hash_rng.h — counter-based hash RNG shared by the CPU and
4+
// CUDA backends (and by tests that need to reproduce an op's noise).
5+
//
6+
// A stateless, order-independent uniform generator: the uniform for element
7+
// `index` of stream (`seed`, `domain`) is a pure function of those three
8+
// integers, so a CPU loop and a CUDA grid produce bit-identical noise for
9+
// bit-identical inputs. The mixer is splitmix64 (Steele/Lea/Flood) — a
10+
// bijective 64-bit finaliser with full avalanche, which is exactly what a
11+
// counter-based generator needs; the input is `seed ^ (domain + index)` so a
12+
// caller varies `seed` per step and each op keeps a distinct `domain` for
13+
// each of its noise streams (a position stream and a vocabulary stream must
14+
// not collide at index 0).
15+
//
16+
// The uniform takes the high 24 bits of the hash to a float in [0, 1) — the
17+
// same mapping the Philox ops use (rand_uniform, sample_logits), so the value
18+
// set is identical: multiples of 2^-24, never 1.0.
19+
//
20+
// Every function is `inline` and, under nvcc, `__host__ __device__`, so the
21+
// header compiles into both a .cpp and a .cu translation unit with identical
22+
// integer arithmetic. Metal cannot include this header (MSL is compiled from
23+
// source strings); src/metal/masked_diffusion.mm carries a verbatim copy.
24+
25+
#include <cstdint>
26+
27+
#if defined(__CUDACC__)
28+
#define BROTENSOR_HASH_RNG_HD __host__ __device__
29+
#else
30+
#define BROTENSOR_HASH_RNG_HD
31+
#endif
32+
33+
namespace brotensor::detail {
34+
35+
// Domain constants for the ops that draw from this generator. Arbitrary
36+
// large odd constants, far apart, so `domain + index` never overlaps across
37+
// streams for any index a tensor can hold.
38+
inline constexpr std::uint64_t kHashDomainMaskedDiffusionPosition = 0x5851F42D4C957F2DULL;
39+
inline constexpr std::uint64_t kHashDomainMaskedDiffusionClass = 0x14057B7EF767814FULL;
40+
41+
// splitmix64 finaliser: one round of the SplitMix64 output function applied
42+
// to `x` (including the golden-ratio increment, so x == 0 does not map to 0).
43+
BROTENSOR_HASH_RNG_HD inline std::uint64_t splitmix64(std::uint64_t x) {
44+
x += 0x9E3779B97F4A7C15ULL;
45+
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
46+
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
47+
return x ^ (x >> 31);
48+
}
49+
50+
// Raw 64-bit hash for element `index` of stream (seed, domain).
51+
BROTENSOR_HASH_RNG_HD inline std::uint64_t hash_u64(std::uint64_t seed,
52+
std::uint64_t domain,
53+
std::uint64_t index) {
54+
return splitmix64(seed ^ (domain + index));
55+
}
56+
57+
// Uniform in [0, 1): top 24 bits of the hash / 2^24. Exact in FP32.
58+
BROTENSOR_HASH_RNG_HD inline float hash_uniform(std::uint64_t seed,
59+
std::uint64_t domain,
60+
std::uint64_t index) {
61+
return static_cast<float>(hash_u64(seed, domain, index) >> 40) *
62+
(1.0f / 16777216.0f);
63+
}
64+
65+
} // namespace brotensor::detail
66+
67+
#undef BROTENSOR_HASH_RNG_HD

include/brotensor/detail/op_table.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,15 @@
624624
X(sample_logits_into, void, (const ::brotensor::Tensor& logits, float temperature, int top_k, float top_p, \
625625
uint64_t key, ::brotensor::Tensor& counter, ::brotensor::Tensor& scratch, \
626626
::brotensor::Tensor& indices)) \
627+
/* ─── Masked-diffusion token selection (OmniVoice codebook grids) ─── */ \
628+
X(masked_diffusion_scores, void, (const ::brotensor::Tensor& logits, const ::brotensor::Tensor& tokens, \
629+
int T, int C, int V, int mask_id, \
630+
float guidance_scale, float layer_penalty, \
631+
float position_temperature, float class_temperature, \
632+
float class_top_frac, uint64_t seed, \
633+
::brotensor::Tensor& pred, ::brotensor::Tensor& scores)) \
634+
X(masked_diffusion_commit, void, (const ::brotensor::Tensor& pred, const ::brotensor::Tensor& idx, int k, int step, \
635+
::brotensor::Tensor& tokens, ::brotensor::Tensor& unmask_step)) \
627636
/* ─── L2 norm + Gated Delta Rule (linear-attention text path) ─── */ \
628637
X(l2_norm_forward, void, (const ::brotensor::Tensor& X, int head_dim, int num_heads, float eps, \
629638
::brotensor::Tensor& Y)) \

include/brotensor/ops/sampling.h

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,82 @@ void randn_truncated(float lo, float hi,
118118
uint64_t key, uint64_t counter,
119119
Tensor& Y);
120120

121+
122+
// ─── Masked-diffusion token selection (OmniVoice-style codebook grids) ─────
123+
//
124+
// One step of a masked-diffusion language model over a (C codebooks, T frames)
125+
// token grid with vocabulary V, where id `mask_id` marks a still-masked cell.
126+
// Two ops split the step: `masked_diffusion_scores` fuses classifier-free
127+
// guidance, log-softmax, per-cell prediction and the confidence score every
128+
// cell competes with; the host then picks the k best cells (top_k_rows over
129+
// the scores viewed as one (1, C*T) row) and `masked_diffusion_commit` writes
130+
// the chosen predictions back. FP32-only, implemented on CPU, CUDA and Metal.
131+
//
132+
// Math per cell (c, t), with c_logits / u_logits its conditional and
133+
// unconditional logit rows (upstream: OmniVoice._predict_tokens_with_scoring):
134+
// if guidance_scale != 0:
135+
// c = log_softmax(c_logits); u = log_softmax(u_logits)
136+
// log_probs = log_softmax(c + guidance_scale * (c - u))
137+
// else:
138+
// log_probs = log_softmax(c_logits)
139+
// log_probs[mask_id] = -inf
140+
// if class_temperature > 0:
141+
// k = ceil(class_top_frac * V), clamped to [1, V]
142+
// filtered = log_probs with all but its k largest entries set to -inf
143+
// (ties at the k-th value keep the lower vocabulary index)
144+
// pred = argmax(filtered / class_temperature + gumbel(u_class[v]))
145+
// else:
146+
// pred = argmax(log_probs) (ties: lowest index)
147+
// confidence = max(log_probs) (UNfiltered, always)
148+
// score = confidence - c * layer_penalty
149+
// if position_temperature > 0:
150+
// score = score / position_temperature + gumbel(u_pos)
151+
// score = -inf where tokens[c, t] != mask_id (already decided)
152+
// gumbel(u) = -log(-log(u + 1e-10) + 1e-10), u ~ U[0, 1)
153+
//
154+
// Noise: the uniforms are a counter-based hash (detail/hash_rng.h,
155+
// splitmix64) of (seed, cell index c*T + t) for u_pos and of (seed, cell
156+
// index * V + v) for u_class, under two distinct domain constants — so the
157+
// CPU and CUDA backends draw bit-identical noise for the same `seed`, and the
158+
// caller gets fresh noise by varying `seed` per step. Nothing is consumed or
159+
// advanced; the same (inputs, seed) always yields the same result.
160+
//
161+
// Numerics: log-sum-exp accumulates in FP64 on CPU and CUDA (Metal, which has
162+
// no FP64, accumulates in FP32); everything else is FP32 with the same
163+
// operation order on every backend, so CPU and CUDA agree to the last ulp
164+
// except where their libm exp/log differ, and `pred` can only diverge on an
165+
// exact FP32 near-tie.
166+
//
167+
// logits: (R, C*V) FP32; column c*V + v. R == 2*T when guidance_scale != 0
168+
// (rows [0, T) conditional, rows [T, 2T) unconditional), R == T
169+
// otherwise.
170+
// tokens: (C, T) INT32 current grid; cell (c, t) is at c*T + t and is masked
171+
// iff tokens == mask_id.
172+
// pred: (C, T) INT32 output, resized + dtype-set. The predicted id for
173+
// every cell (never mask_id unless the whole row is -inf).
174+
// scores: (C, T) FP32 output, resized + dtype-set. -inf where tokens !=
175+
// mask_id.
176+
// Throws ("brotensor: masked_diffusion_scores: <reason>") for a non-FP32
177+
// logits / non-INT32 tokens, T/C/V < 1, mask_id outside [0, V), or a shape
178+
// that does not match (T, C, V, guidance_scale).
179+
void masked_diffusion_scores(const Tensor& logits, const Tensor& tokens,
180+
int T, int C, int V, int mask_id,
181+
float guidance_scale, float layer_penalty,
182+
float position_temperature, float class_temperature,
183+
float class_top_frac, std::uint64_t seed,
184+
Tensor& pred, Tensor& scores);
185+
186+
187+
// Commit the k selected cells: for i in [0, k): p = idx[i]; tokens[p] =
188+
// pred[p]; unmask_step[p] = step. `idx` holds flat cell indices c*T + t into
189+
// the (C, T) grids — the layout top_k_rows returns over `scores` viewed as
190+
// (1, C*T). tokens / unmask_step / pred are (C, T) INT32 (all pre-sized,
191+
// nothing is resized); idx is INT32 with at least k elements, of which only
192+
// the first k are read. k == 0 is a no-op. An index outside [0, C*T) is
193+
// ignored on every backend (a device kernel cannot throw), so the host should
194+
// only ever pass what top_k_rows produced. Throws for a dtype / shape / k
195+
// mismatch.
196+
void masked_diffusion_commit(const Tensor& pred, const Tensor& idx, int k, int step,
197+
Tensor& tokens, Tensor& unmask_step);
198+
121199
} // namespace brotensor

0 commit comments

Comments
 (0)