|
| 1 | +# ===----------------------------------------------------------------------=== # |
| 2 | +# Copyright (c) 2026, Modular Inc. All rights reserved. |
| 3 | +# |
| 4 | +# Licensed under the Apache License v2.0 with LLVM Exceptions: |
| 5 | +# https://llvm.org/LICENSE.txt |
| 6 | +# |
| 7 | +# Unless required by applicable law or agreed to in writing, software |
| 8 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 9 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 10 | +# See the License for the specific language governing permissions and |
| 11 | +# limitations under the License. |
| 12 | +# ===----------------------------------------------------------------------=== # |
| 13 | +"""Depthwise causal short convolution over a ring of past inputs. |
| 14 | +
|
| 15 | +A request's conv state is a ring indexed by absolute position: the input at |
| 16 | +position `p` lives at `ring[p % R]`. A forward is two launches that never |
| 17 | +touch the same entry: |
| 18 | +
|
| 19 | +- `short_conv_ring_fwd`: `x + conv(x)`. A tap at `p - j` reads `x` when it |
| 20 | + is inside the chunk, the ring when it is before the chunk, and zero before |
| 21 | + position zero. Writes nothing. |
| 22 | +- `short_conv_ring_commit`: writes each sequence's last `R` inputs into |
| 23 | + their entries. Reads nothing. |
| 24 | +
|
| 25 | +One code path therefore covers decode, prefill, mixed batches and |
| 26 | +speculative verify. A rejected draft token's entry is overwritten once the |
| 27 | +sequence passes its position again, so `R` must be `width - 1` plus the |
| 28 | +largest rollback. |
| 29 | +""" |
| 30 | + |
| 31 | +from std.math import ceildiv |
| 32 | +from std.utils.numerics import get_accum_type |
| 33 | + |
| 34 | +from max.gpu import block_dim, block_idx, thread_idx |
| 35 | +from max.gpu.host import DeviceContext |
| 36 | +from max.gpu.host.info import is_gpu |
| 37 | +from layout import TileTensor |
| 38 | +from layout.coord import Coord |
| 39 | +from max.runtime.tracing import Trace, TraceLevel, get_safe_task_id |
| 40 | + |
| 41 | +from nn._ragged_utils import get_batch_and_token_idx_from_row_offsets |
| 42 | + |
| 43 | +comptime _BLOCK = 256 |
| 44 | +"""Channels one block covers.""" |
| 45 | + |
| 46 | + |
| 47 | +@inline(.always) |
| 48 | +def _short_conv_ring_step[ |
| 49 | + x_dtype: DType, ring_dtype: DType, //, width: Int, accum_dtype: DType |
| 50 | +]( |
| 51 | + x: TileTensor[mut=False, x_dtype, ...], |
| 52 | + weight: TileTensor[mut=False, x_dtype, ...], |
| 53 | + ring: TileTensor[ring_dtype, ...], |
| 54 | + token_idx: Int, |
| 55 | + channel: Int, |
| 56 | + slot: Int, |
| 57 | + position: Int, |
| 58 | + chunk_start: Int, |
| 59 | +) -> Scalar[accum_dtype]: |
| 60 | + """`x + conv(x)` for one token of one channel. |
| 61 | +
|
| 62 | + `x[token_idx, channel]` is the token's input. Tap `j` reads position |
| 63 | + `position - j` from `x` when it is at or after `chunk_start`, from |
| 64 | + `ring[slot, (position - j) % R, channel]` when it is before the chunk, |
| 65 | + else zero. `weight[channel, width - 1]` multiplies the current input, |
| 66 | + `weight[channel, width - 1 - j]` the input `j` back, as in |
| 67 | + `causal_conv1d`. |
| 68 | + """ |
| 69 | + comptime ring_len = ring.static_shape[1] |
| 70 | + comptime assert ring_len != -1, "the conv ring needs a static length" |
| 71 | + comptime assert ( |
| 72 | + ring_len >= width - 1 |
| 73 | + ), "the conv ring must hold at least width - 1 positions of history" |
| 74 | + |
| 75 | + @inline(.always) |
| 76 | + def tap(w: Int) {imm} -> Scalar[accum_dtype]: |
| 77 | + return weight.load[width=1](Coord(channel, w)).cast[accum_dtype]()[0] |
| 78 | + |
| 79 | + var x_cur = x.load[width=1](Coord(token_idx, channel)).cast[accum_dtype]()[ |
| 80 | + 0 |
| 81 | + ] |
| 82 | + var acc = x_cur + x_cur * tap(width - 1) |
| 83 | + comptime for j in range(1, width): |
| 84 | + var tap_pos = position - j |
| 85 | + var value = Scalar[accum_dtype](0) |
| 86 | + if tap_pos >= chunk_start: |
| 87 | + value = x.load[width=1](Coord(token_idx - j, channel)).cast[ |
| 88 | + accum_dtype |
| 89 | + ]()[0] |
| 90 | + elif tap_pos >= 0: |
| 91 | + value = ring.load[width=1]( |
| 92 | + Coord(slot, tap_pos % ring_len, channel) |
| 93 | + ).cast[accum_dtype]()[0] |
| 94 | + acc += value * tap(width - 1 - j) |
| 95 | + return acc |
| 96 | + |
| 97 | + |
| 98 | +def short_conv_ring_fwd[ |
| 99 | + dtype: DType, |
| 100 | + ring_dtype: DType, |
| 101 | + //, |
| 102 | + target: StaticString, |
| 103 | +]( |
| 104 | + x: TileTensor[mut=False, dtype, ...], |
| 105 | + weight: TileTensor[mut=False, dtype, ...], |
| 106 | + ring: TileTensor[ring_dtype, ...], |
| 107 | + input_row_offsets: TileTensor[mut=False, .uint32, ...], |
| 108 | + positions: TileTensor[mut=False, .uint32, ...], |
| 109 | + conv_row: TileTensor[mut=False, .uint32, ...], |
| 110 | + output: TileTensor[mut=True, dtype, ...], |
| 111 | + context: DeviceContext, |
| 112 | +) raises: |
| 113 | + """`x + conv(x)` over a ragged batch; reads the ring, writes nothing. |
| 114 | +
|
| 115 | + `x`, `output`: `[total_seq_len, channels]`. `weight`: `[channels, |
| 116 | + width]`. `ring`: `[slots, R, channels]`. `positions` is per token, |
| 117 | + `conv_row` per sequence. |
| 118 | + """ |
| 119 | + comptime assert is_gpu[target](), "short_conv_ring_fwd is GPU-only" |
| 120 | + comptime assert x.flat_rank == 2, "x must be [total_seq_len, channels]" |
| 121 | + comptime assert output.flat_rank == 2, "output must match x" |
| 122 | + comptime assert weight.flat_rank == 2, "weight must be [channels, width]" |
| 123 | + comptime assert ring.flat_rank == 3, "ring must be [slots, R, channels]" |
| 124 | + comptime width = weight.static_shape[1] |
| 125 | + comptime assert width != -1, "Need static shape for weight" |
| 126 | + comptime accum_dtype = get_accum_type[dtype]() |
| 127 | + |
| 128 | + var total_seq_len = Int(x.dim[0]()) |
| 129 | + var channels = Int(x.dim[1]()) |
| 130 | + if total_seq_len == 0: |
| 131 | + return |
| 132 | + # Kernel captures must be fixed-width; Int is not device-passable. |
| 133 | + var channel_blocks = Int32(ceildiv(channels, _BLOCK)) |
| 134 | + var channels_dev = Int32(channels) |
| 135 | + |
| 136 | + def kernel() { |
| 137 | + var x, |
| 138 | + var weight, |
| 139 | + var ring, |
| 140 | + var input_row_offsets, |
| 141 | + var positions, |
| 142 | + var conv_row, |
| 143 | + var output, |
| 144 | + var channel_blocks, |
| 145 | + var channels_dev, |
| 146 | + }: |
| 147 | + var token_idx = Int(block_idx.x) // Int(channel_blocks) |
| 148 | + var channel = (Int(block_idx.x) % Int(channel_blocks)) * Int( |
| 149 | + block_dim.x |
| 150 | + ) + Int(thread_idx.x) |
| 151 | + if channel >= Int(channels_dev): |
| 152 | + return |
| 153 | + var batch_idx, idx_in_seq = get_batch_and_token_idx_from_row_offsets( |
| 154 | + input_row_offsets, token_idx |
| 155 | + ) |
| 156 | + var position = Int(positions.load[width=1](Coord(token_idx))[0]) |
| 157 | + var value = _short_conv_ring_step[width, accum_dtype]( |
| 158 | + x, |
| 159 | + weight, |
| 160 | + ring, |
| 161 | + token_idx, |
| 162 | + channel, |
| 163 | + Int(conv_row.load[width=1](Coord(batch_idx))[0]), |
| 164 | + position, |
| 165 | + position - idx_in_seq, |
| 166 | + ) |
| 167 | + output.store[width=1](Coord(token_idx, channel), value.cast[dtype]()) |
| 168 | + |
| 169 | + with Trace[TraceLevel.OP,> |
| 170 | + "short_conv_ring_fwd.channels_" + String(channels), |
| 171 | + task_id=get_safe_task_id(context), |
| 172 | + ): |
| 173 | + context.enqueue_function( |
| 174 | + kernel, |
| 175 | + grid_dim=total_seq_len * Int(channel_blocks), |
| 176 | + block_dim=_BLOCK, |
| 177 | + ) |
| 178 | + |
| 179 | + |
| 180 | +@inline(.always) |
| 181 | +def _commit_sequence_tail[ |
| 182 | + x_dtype: DType, // |
| 183 | +]( |
| 184 | + x: TileTensor[mut=False, x_dtype, ...], |
| 185 | + ring: TileTensor[mut=True, ...], |
| 186 | + input_row_offsets: TileTensor[mut=False, .uint32, ...], |
| 187 | + positions: TileTensor[mut=False, .uint32, ...], |
| 188 | + conv_row: TileTensor[mut=False, .uint32, ...], |
| 189 | + batch_idx: Int, |
| 190 | + channel: Int, |
| 191 | +): |
| 192 | + """Writes one channel of a sequence's last `R` inputs into its slot. |
| 193 | +
|
| 194 | + Earlier inputs share entries with later ones, so only the tail is |
| 195 | + written and every entry has one writer. |
| 196 | + """ |
| 197 | + comptime ring_len = ring.static_shape[1] |
| 198 | + comptime assert ring_len != -1, "the conv ring needs a static length" |
| 199 | + |
| 200 | + var start = Int(input_row_offsets.load[width=1](Coord(batch_idx))[0]) |
| 201 | + var end = Int(input_row_offsets.load[width=1](Coord(batch_idx + 1))[0]) |
| 202 | + var slot = Int(conv_row.load[width=1](Coord(batch_idx))[0]) |
| 203 | + for token_idx in range(max(start, end - ring_len), end): |
| 204 | + var position = Int(positions.load[width=1](Coord(token_idx))[0]) |
| 205 | + var value = x.load[width=1](Coord(token_idx, channel)) |
| 206 | + ring.store[width=1]( |
| 207 | + Coord(slot, position % ring_len, channel), |
| 208 | + value.cast[ring.dtype](), |
| 209 | + ) |
| 210 | + |
| 211 | + |
| 212 | +def short_conv_ring_commit[ |
| 213 | + dtype: DType, |
| 214 | + //, |
| 215 | + target: StaticString, |
| 216 | +]( |
| 217 | + x: TileTensor[mut=False, dtype, ...], |
| 218 | + ring: TileTensor[mut=True, ...], |
| 219 | + input_row_offsets: TileTensor[mut=False, .uint32, ...], |
| 220 | + positions: TileTensor[mut=False, .uint32, ...], |
| 221 | + conv_row: TileTensor[mut=False, .uint32, ...], |
| 222 | + context: DeviceContext, |
| 223 | +) raises: |
| 224 | + """Writes each sequence's last `R` rows of `x` into its ring slot. |
| 225 | +
|
| 226 | + `x`: `[total_seq_len, channels]`. `ring`: `[slots, R, channels]`. |
| 227 | + Launch after every reader of the ring in the same forward. |
| 228 | + """ |
| 229 | + comptime assert is_gpu[target](), "short_conv_ring_commit is GPU-only" |
| 230 | + comptime assert x.flat_rank == 2, "x must be [total_seq_len, channels]" |
| 231 | + comptime assert ring.flat_rank == 3, "ring must be [slots, R, channels]" |
| 232 | + comptime channels = ring.static_shape[2] |
| 233 | + comptime assert channels != -1, "the conv ring needs a static width" |
| 234 | + |
| 235 | + var batch_size = Int(input_row_offsets.dim[0]()) - 1 |
| 236 | + if batch_size <= 0 or x.dim[0]() == 0: |
| 237 | + return |
| 238 | + |
| 239 | + def kernel() { |
| 240 | + var x, |
| 241 | + var ring, |
| 242 | + var input_row_offsets, |
| 243 | + var positions, |
| 244 | + var conv_row, |
| 245 | + }: |
| 246 | + var channel = Int(block_idx.x) * Int(block_dim.x) + Int(thread_idx.x) |
| 247 | + if channel >= channels: |
| 248 | + return |
| 249 | + _commit_sequence_tail( |
| 250 | + x, |
| 251 | + ring, |
| 252 | + input_row_offsets, |
| 253 | + positions, |
| 254 | + conv_row, |
| 255 | + Int(block_idx.y), |
| 256 | + channel, |
| 257 | + ) |
| 258 | + |
| 259 | + with Trace[TraceLevel.OP,> |
| 260 | + "short_conv_ring_commit.channels_" + String(channels), |
| 261 | + task_id=get_safe_task_id(context), |
| 262 | + ): |
| 263 | + context.enqueue_function( |
| 264 | + kernel, |
| 265 | + grid_dim=(ceildiv(channels, _BLOCK), batch_size), |
| 266 | + block_dim=_BLOCK, |
| 267 | + ) |
0 commit comments