File size: 15,057 Bytes
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
"""Slot-based, static-state, CUDA-graphed one-frame Mimi decoder.

Arithmetic mirrors serve/codec.py::StreamingMimi (the verified reference): causal
convolution history, transposed-convolution overlap tails (bias removed from the
stored tail), and sliding-window attention with absolute RoPE positions. The
difference is where state lives: every stateful layer owns ONE static tensor with
a row per slot (row ``max_slots`` is a dummy used to pad a batch to its bucket),
and the decoder transformer runs against static per-slot KV ring buffers, so one
decode is a single CUDA-graph replay with no per-seat Python work.

No upstream module or parameter is modified; HF sub-modules are called for their
weights. Global side effect: warmup() sets torch.backends.cudnn.benchmark=False so
convolution algorithm choice is settled before capture. Matmul/cuDNN TF32 flags
are read at capture time and baked into the graphs.

Sliding-window semantics (HF create_sliding_window_causal_mask, reproduced by
StreamingMimi keeping sliding_window-1 cached tokens plus the new ones): a query
at absolute position p sees keys with  p - sliding_window < key_pos <= p.
"""
import torch
import torch.nn.functional as F
from transformers.models.mimi.modeling_mimi import (MimiConv1d, MimiConvTranspose1d, MimiResnetBlock,
                                                    apply_rotary_pos_emb, repeat_kv)

NEVER = -(1 << 40)  # key_pos of an empty ring entry; always outside every window


class _Bucket:
    __slots__ = ('graph', 'slots', 'codes', 'out', 'pinned', 'events', 'turn')


class StaticMimi:
    def __init__(self, mimi, max_slots, batch_buckets=(1, 2, 4, 8, 16, 24, 32, 48, 64, 80, 96), ring=256):
        cfg = mimi.config
        if not cfg.use_causal_conv or cfg.pad_mode != 'constant' or cfg.trim_right_ratio != 1:
            raise ValueError('Static decoder requires causal constant padding and trim_right_ratio=1')
        if mimi.upsample is None:
            raise ValueError('Static decoder expects the Mimi upsample layer')
        self.m = mimi
        self.max_slots = int(max_slots)
        self.dummy = self.max_slots
        self.batch_buckets = tuple(sorted(set(int(b) for b in batch_buckets)))
        self.window = int(cfg.sliding_window)
        self.frame_tokens = int(mimi.upsample.conv.stride[0])
        self.ring = int(ring)
        if self.ring < self.window + self.frame_tokens:
            raise ValueError('ring must hold sliding_window + frame_tokens entries')
        parameter = next(mimi.parameters())
        self.device, self.dtype = parameter.device, parameter.dtype
        self.codebooks = int(cfg.num_quantizers)
        with torch.inference_mode():
            # Convolution history / overlap tails: one flat row per slot, one view per stateful layer.
            self._spec = {}
            self._plan(mimi.upsample, 'upsample')
            for i, layer in enumerate(mimi.decoder.layers):
                self._plan(layer, f'decoder.{i}')
            total = sum(c * p for c, p in self._spec.values())
            rows = self.max_slots + 1
            self.conv_flat = torch.zeros(rows, total, device=self.device, dtype=self.dtype)
            self.state, offset = {}, 0
            for key, (channels, pad) in self._spec.items():
                self.state[key] = self.conv_flat[:, offset:offset + channels * pad].view(rows, channels, pad)
                offset += channels * pad
            layers = mimi.decoder_transformer.layers
            attn = layers[0].self_attn
            self.kv_heads, self.head_dim, self.kv_groups = attn.num_key_value_heads, attn.head_dim, attn.num_key_value_groups
            # [layer, k/v, slot, heads, ring, head_dim]; keys are stored post-RoPE.
            self.kv = torch.zeros(len(layers), 2, rows, self.kv_heads, self.ring, self.head_dim,
                                  device=self.device, dtype=self.dtype)
            self.key_pos = torch.full((rows, self.ring), NEVER, device=self.device, dtype=torch.long)
            self.tokens = torch.zeros(rows, device=self.device, dtype=torch.long)
            self._steps = torch.arange(self.frame_tokens, device=self.device)
            rotary = attn.rotary_emb
            self._inv_freq = rotary.inv_freq.detach().float().clone()
            self._rope_scale = float(rotary.attention_scaling)
        self._pool = None
        self._buckets = {}
        self.samples_per_frame = None

    # ---- state layout -------------------------------------------------------------------------
    def _plan(self, module, key):
        if isinstance(module, MimiConv1d):
            if module.conv.stride != (1,):
                raise ValueError('Only stride-one decoder convolutions are supported')
            padding = (module.conv.kernel_size[0] - 1) * module.conv.dilation[0]
            if padding:
                self._spec[key] = (module.conv.in_channels, padding)
        elif isinstance(module, MimiConvTranspose1d):
            if module.padding_left != 0 or module.conv.dilation != (1,):
                raise ValueError('Unsupported transpose-convolution geometry')
            if module.padding_right:
                self._spec[key] = (module.conv.out_channels, module.padding_right)
        elif isinstance(module, MimiResnetBlock):
            self._plan(module.shortcut, key + '.shortcut')
            for i, child in enumerate(module.block):
                self._plan(child, key + f'.block.{i}')

    def state_bytes_per_slot(self):
        return {'conv': self.conv_flat[0].numel() * self.conv_flat.element_size(),
                'attention_kv': self.kv[:, :, 0].numel() * self.kv.element_size(),
                'attention_index': self.key_pos[0].numel() * 8 + 8}

    # ---- graph body ---------------------------------------------------------------------------
    def _layer(self, module, x, slots, key):
        if isinstance(module, MimiConv1d):
            if key in self.state:
                state = self.state[key]
                joined = torch.cat((state.index_select(0, slots), x), dim=-1)
                state.index_copy_(0, slots, joined[..., -state.shape[-1]:])
                x = joined
            return module.conv(x)
        if isinstance(module, MimiConvTranspose1d):
            y = module.conv(x)
            produced = x.shape[-1] * module.conv.stride[0]
            if key in self.state:
                state = self.state[key]
                overlap = state.shape[-1]
                if overlap > produced:
                    raise ValueError('Chunk too short for transpose-convolution overlap')
                previous = state.index_select(0, slots)
                tail = y[..., produced:]
                if module.conv.bias is not None:
                    tail = tail - module.conv.bias[None, :, None]
                state.index_copy_(0, slots, tail)
                y[..., :overlap] += previous          # disjoint from the tail because overlap <= produced
            return y[..., :produced]
        if isinstance(module, MimiResnetBlock):
            shortcut = self._layer(module.shortcut, x, slots, key + '.shortcut')
            for i, child in enumerate(module.block):
                x = self._layer(child, x, slots, key + f'.block.{i}')
            return shortcut + x
        return module(x)

    @staticmethod
    def _rvq(rvq, codes):
        total = None
        for i in range(codes.shape[1]):
            e = F.embedding(codes[:, i], rvq.layers[i].codebook.embed).permute(0, 2, 1)
            total = e if total is None else total + e
        return rvq.output_proj(total) if rvq.output_proj is not None else total

    def _quantizer(self, codes):
        # Same arithmetic as MimiSplitResidualVectorQuantizer.decode, without its in-graph torch.tensor(0.0).
        q = self.m.quantizer
        n = q.num_semantic_quantizers
        out = self._rvq(q.semantic_residual_vector_quantizer, codes[:, :n])
        if codes.shape[1] > n:
            out = out + self._rvq(q.acoustic_residual_vector_quantizer, codes[:, n:])
        return out

    def _transformer(self, h, slots):
        batch, count, _ = h.shape
        start = self.tokens.index_select(0, slots)
        positions = start[:, None] + self._steps[None]                      # [B,T] absolute
        ring_index = positions % self.ring
        rows = slots[:, None]
        self.key_pos.index_put_((rows, ring_index), positions)
        key_pos = self.key_pos.index_select(0, slots)[:, None, :]            # [B,1,R]
        query_pos = positions[:, :, None]                                    # [B,T,1]
        mask = ((key_pos <= query_pos) & (key_pos > query_pos - self.window))[:, None]   # [B,1,T,R]
        freqs = positions[:, :, None].float() * self._inv_freq[None, None, :]
        emb = torch.cat((freqs, freqs), dim=-1)
        cos = (emb.cos() * self._rope_scale).to(h.dtype)
        sin = (emb.sin() * self._rope_scale).to(h.dtype)
        for li, layer in enumerate(self.m.decoder_transformer.layers):
            attn = layer.self_attn
            x = layer.input_layernorm(h)
            q = attn.q_proj(x).view(batch, count, attn.num_heads, self.head_dim).transpose(1, 2)
            k = attn.k_proj(x).view(batch, count, self.kv_heads, self.head_dim).transpose(1, 2)
            v = attn.v_proj(x).view(batch, count, self.kv_heads, self.head_dim).transpose(1, 2)
            q, k = apply_rotary_pos_emb(q, k, cos, sin)
            keys, values = self.kv[li, 0], self.kv[li, 1]
            # Advanced indices on dims 0 and 2 put the indexed dims first: value layout [B,T,heads,dim].
            keys[rows, :, ring_index] = k.transpose(1, 2)
            values[rows, :, ring_index] = v.transpose(1, 2)
            k_all = repeat_kv(keys.index_select(0, slots), self.kv_groups)
            v_all = repeat_kv(values.index_select(0, slots), self.kv_groups)
            a = F.scaled_dot_product_attention(q, k_all, v_all, attn_mask=mask)
            a = attn.o_proj(a.transpose(1, 2).reshape(batch, count, -1))
            h = h + layer.self_attn_layer_scale(a)
            h = h + layer.mlp_layer_scale(layer.mlp(layer.post_attention_layernorm(h)))
        self.tokens.index_copy_(0, slots, start + count)
        self.tokens[self.dummy].zero_()
        return h

    def _forward(self, slots, codes):
        x = self._quantizer(codes)
        x = self._layer(self.m.upsample, x, slots, 'upsample')
        x = self._transformer(x.transpose(1, 2), slots).transpose(1, 2)
        for i, layer in enumerate(self.m.decoder.layers):
            x = self._layer(layer, x, slots, f'decoder.{i}')
        return x

    # ---- capture ------------------------------------------------------------------------------
    def _capture(self, size):
        bucket = _Bucket()
        bucket.slots = torch.full((size,), self.dummy, device=self.device, dtype=torch.long)
        bucket.codes = torch.zeros(size, self.codebooks, 1, device=self.device, dtype=torch.long)
        bucket.pinned = [torch.full((size,), self.dummy, dtype=torch.long).pin_memory() for _ in range(8)]
        bucket.events = [None] * len(bucket.pinned)
        bucket.turn = 0
        # Warmup and capture touch only the dummy slot, so live seats are never disturbed.
        side = torch.cuda.Stream(device=self.device)
        side.wait_stream(torch.cuda.current_stream(self.device))
        with torch.cuda.stream(side):
            for _ in range(3):
                self._forward(bucket.slots, bucket.codes)
        torch.cuda.current_stream(self.device).wait_stream(side)
        torch.cuda.synchronize(self.device)
        if self._pool is None:
            self._pool = torch.cuda.graph_pool_handle()
        bucket.graph = torch.cuda.CUDAGraph()
        with torch.cuda.graph(bucket.graph, pool=self._pool):
            bucket.out = self._forward(bucket.slots, bucket.codes)
        if self.samples_per_frame is None:
            self.samples_per_frame = bucket.out.shape[-1]
        self._reset_row(self.dummy)
        self._buckets[size] = bucket
        return bucket

    @torch.inference_mode()
    def warmup(self, batch_buckets=None):
        torch.backends.cudnn.benchmark = False
        sizes = self.batch_buckets if batch_buckets is None else tuple(sorted(set(int(b) for b in batch_buckets)))
        for size in sorted(sizes, reverse=True):      # largest first so the shared pool is sized once
            if size not in self._buckets:
                self._capture(size)
        self.batch_buckets = tuple(sorted(set(self.batch_buckets) | set(sizes)))

    # ---- public -------------------------------------------------------------------------------
    def _reset_row(self, row):
        self.conv_flat[row].zero_()
        self.key_pos[row].fill_(NEVER)
        self.tokens[row].zero_()

    @torch.inference_mode()
    def reset(self, slot, zero_kv=False):
        """Three fill kernels, no host sync. KV rows are unreachable once key_pos is NEVER
        (masked weights are exactly zero); zero_kv=True additionally clears them."""
        if not 0 <= slot < self.max_slots:
            raise IndexError(f'slot {slot} outside [0,{self.max_slots})')
        self._reset_row(slot)
        if zero_kv:
            self.kv[:, :, slot].zero_()

    @torch.inference_mode()
    def decode(self, slots, codes):
        """slots: distinct ints in [0,max_slots) (or a CUDA long tensor of them); codes: [B,K] long CUDA,
        one frame per seat. Returns an owned float32 [B, samples_per_frame] tensor."""
        count = len(slots)
        if codes.shape[0] != count or codes.dim() != 2 or codes.shape[1] != self.codebooks:
            raise ValueError(f'codes must be [{count},{self.codebooks}], got {tuple(codes.shape)}')
        size = next((b for b in self.batch_buckets if b >= count), None)
        if size is None:
            raise ValueError(f'batch {count} exceeds largest bucket {self.batch_buckets[-1]}')
        bucket = self._buckets.get(size) or self._capture(size)
        if torch.is_tensor(slots):
            bucket.slots[:count].copy_(slots)
            bucket.slots[count:].fill_(self.dummy)
        else:
            if len(set(slots)) != count or min(slots) < 0 or max(slots) >= self.max_slots:
                raise ValueError('slots must be distinct and inside [0,max_slots)')
            # Rotate pinned staging rows so an async H2D copy is never overwritten before it runs.
            turn = bucket.turn
            bucket.turn = (turn + 1) % len(bucket.pinned)
            if bucket.events[turn] is not None:
                bucket.events[turn].synchronize()
            staging = bucket.pinned[turn]
            staging[:count] = torch.tensor(slots, dtype=torch.long)
            staging[count:] = self.dummy
            bucket.slots.copy_(staging, non_blocking=True)
            event = torch.cuda.Event()
            event.record()
            bucket.events[turn] = event
        bucket.codes[:count, :, 0].copy_(codes)
        bucket.graph.replay()
        return bucket.out[:count, 0].float().clone() if bucket.out.dtype != torch.float32 else bucket.out[:count, 0].clone()