Skip to main content

logicaffeine_compile/spatial/decode_engine/rtl/
stream.rs

1//! The image stream engine: one memory channel's tiles of the weight image in, the tiles' rows out as 64-byte beats.
2//!
3//! A tile descriptor names a tile of [`super::super::image::Image`]: its first line on the channel, its lines, the bytes
4//! of rows it holds, its tensor and its first row. The engine sits in the compute clock on the shell's channel
5//! interface: it asks the shell for the tile's lines with one read descriptor, takes the lines as they cross back, and
6//! sends the tile's row bytes on as beats of 64 bytes, each tagged with the tile's tensor, its first row and the beat's
7//! byte offset in the tile, the last beat carrying how many of its bytes are rows (the rest zero). At 72-byte lines the
8//! bytes are regathered: eight lines make nine beats, so the engine's output runs at the line rate when its clock is
9//! 9/8 of the channel's. With check words every line passes through a CRC-32C as it is taken, and the tile's last beat
10//! says whether the word in its last line's last four bytes was the CRC of the lines before it.
11//!
12//! The beats wait in a 128-byte gather register: a beat leaves once 64 bytes are gathered, a line comes in when its
13//! bytes fit or the register already holds every row byte the tile has left (then the line only feeds the CRC), and
14//! the tile's last beat waits for the tile's last line, so the verdict is known when it leaves. Up to four tiles'
15//! reads are outstanding at once, so a tile's lines follow the last tile's without a gap.
16//!
17//! [`beats`] is the unit's software twin: from a channel's bytes and a descriptor, the beats the RTL sends.
18
19use std::fmt::Write;
20
21use super::super::image::{crc32c, Image, ImageLayout, CHECK_BYTES};
22
23/// Bytes a beat carries.
24pub const BEAT_BYTES: u64 = 64;
25/// Bits of the descriptor fields and tags: the line address, the line count (the shell's descriptor widths), the byte
26/// count, the tensor and the row.
27pub const AW: u32 = 27;
28pub const LW: u32 = 32;
29pub const BYW: u32 = 24;
30pub const TW: u32 = 16;
31pub const ROWW: u32 = 32;
32const GATHER_BYTES: u64 = 128;
33
34/// One tile as the engine takes it.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct TileDescriptor {
37    pub line: u64,
38    pub lines: u64,
39    pub bytes: u64,
40    pub tensor: u32,
41    pub first_row: u64,
42}
43
44/// One beat as the engine sends it: `keep` of its bytes are the tile's rows (the rest zero), `offset` is its first
45/// byte's place in the tile, `crc_error` is set on a tile's last beat when its check word was wrong.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct Beat {
48    pub data: [u8; BEAT_BYTES as usize],
49    pub keep: u64,
50    pub last: bool,
51    pub tensor: u32,
52    pub row: u64,
53    pub offset: u64,
54    pub crc_error: bool,
55}
56
57/// The descriptors of every tile `image` holds on `channel`, in the channel's order.
58pub fn descriptors(image: &Image, channel: u32) -> Vec<TileDescriptor> {
59    image
60        .tiles
61        .iter()
62        .filter(|t| t.channel == channel)
63        .map(|t| TileDescriptor {
64            line: t.line,
65            lines: t.lines,
66            bytes: (t.end_row - t.first_row) * image.tensors[t.tensor].image_row_bytes(),
67            tensor: t.tensor as u32,
68            first_row: t.first_row,
69        })
70        .collect()
71}
72
73/// The beats the engine sends for tile `d` read from `channel` (the channel's bytes) under `layout`.
74pub fn beats(channel: &[u8], d: &TileDescriptor, layout: &ImageLayout) -> Vec<Beat> {
75    let lb = layout.line_bytes as usize;
76    let span = &channel[d.line as usize * lb..(d.line + d.lines) as usize * lb];
77    let crc_error = layout.check && {
78        let body = span.len() - CHECK_BYTES as usize;
79        crc32c(&span[..body]).to_le_bytes() != span[body..]
80    };
81    let rows = &span[..d.bytes as usize];
82    let count = rows.len().div_ceil(BEAT_BYTES as usize);
83    rows.chunks(BEAT_BYTES as usize)
84        .enumerate()
85        .map(|(k, chunk)| {
86            let mut data = [0u8; BEAT_BYTES as usize];
87            data[..chunk.len()].copy_from_slice(chunk);
88            let last = k + 1 == count;
89            Beat {
90                data,
91                keep: chunk.len() as u64,
92                last,
93                tensor: d.tensor,
94                row: d.first_row,
95                offset: k as u64 * BEAT_BYTES,
96                crc_error: last && crc_error,
97            }
98        })
99        .collect()
100}
101
102/// The CRC-32C register after `bytes` from `state`, with no initial or final inversion: the map the hardware applies
103/// to its register a line at a time, linear in the state and the bytes.
104pub fn crc32c_update(mut state: u32, bytes: &[u8]) -> u32 {
105    for &b in bytes {
106        state ^= b as u32;
107        for _ in 0..8 {
108            state = (state >> 1) ^ (0x82F6_3B78 & (state & 1).wrapping_neg());
109        }
110    }
111    state
112}
113
114/// For each bit of the register after `len` bytes, the register bits and the data bits (byte `k`'s bit `j` at bit
115/// `8k + j`) whose exclusive-or it is.
116pub fn crc_masks(len: usize) -> Vec<(u32, Vec<bool>)> {
117    let zeros = vec![0u8; len];
118    let from_state: Vec<u32> = (0..32).map(|j| crc32c_update(1 << j, &zeros)).collect();
119    let from_data: Vec<u32> = (0..8 * len)
120        .map(|k| {
121            let mut d = zeros.clone();
122            d[k / 8] = 1 << (k % 8);
123            crc32c_update(0, &d)
124        })
125        .collect();
126    (0..32)
127        .map(|i| {
128            let state = (0..32).filter(|&j| from_state[j] >> i & 1 == 1).fold(0u32, |m, j| m | 1 << j);
129            (state, from_data.iter().map(|&c| c >> i & 1 == 1).collect())
130        })
131        .collect()
132}
133
134fn hex_mask(bits: &[bool]) -> String {
135    let digits = bits.len().div_ceil(4);
136    (0..digits)
137        .rev()
138        .map(|d| {
139            let nibble = (0..4).fold(0u8, |v, b| v | (u8::from(bits.get(4 * d + b).copied().unwrap_or(false)) << b));
140            char::from_digit(nibble as u32, 16).unwrap()
141        })
142        .collect()
143}
144
145/// The engine for lines of `layout.line_bytes` bytes, checking every tile's CRC-32C when `layout.check`.
146pub fn verilog(layout: &ImageLayout) -> String {
147    let lb = layout.line_bytes;
148    assert!(lb % 8 == 0 && lb >= BEAT_BYTES && lb + 56 <= GATHER_BYTES, "a line of {lb} bytes");
149    let dw = 8 * lb;
150    let mut v = String::new();
151    let _ = writeln!(
152        v,
153        "// The image stream engine for {lb}-byte lines{}: tile descriptors in, the tile's lines asked of the channel as one\n\
154         // read descriptor, its rows out as 64-byte beats tagged with tensor, first row and byte offset.",
155        if layout.check { " with a CRC-32C check word ending every tile" } else { "" }
156    );
157    let _ = writeln!(
158        v,
159        "module logos_image_stream (
160  input              clk,
161  input              rst_n,
162  input              tile_valid,
163  output             tile_ready,
164  input  [{a}:0]      tile_line,
165  input  [{l}:0]      tile_lines,
166  input  [{b}:0]      tile_bytes,
167  input  [{t}:0]      tile_tensor,
168  input  [{r}:0]      tile_row,
169  output             md_valid,
170  input              md_ready,
171  output [{m}:0]      md_data,
172  input              mr_valid,
173  output             mr_ready,
174  input  [{d}:0]     mr_data,
175  output             out_valid,
176  input              out_ready,
177  output [511:0]     out_data,
178  output [6:0]       out_keep,
179  output             out_last,
180  output [{t}:0]      out_tensor,
181  output [{r}:0]      out_row,
182  output [{b}:0]      out_offset,
183  output             out_crc_error
184);",
185        a = AW - 1,
186        l = LW - 1,
187        b = BYW - 1,
188        t = TW - 1,
189        r = ROWW - 1,
190        m = AW + LW,
191        d = dw - 1
192    );
193    let _ = writeln!(
194        v,
195        "  // The tiles whose lines are asked for, oldest first, in registers.
196  (* ramstyle = \"logic\" *) reg [{l}:0] q_lines [0:3];
197  (* ramstyle = \"logic\" *) reg [{b}:0] q_bytes [0:3];
198  (* ramstyle = \"logic\" *) reg [{t}:0] q_tensor [0:3];
199  (* ramstyle = \"logic\" *) reg [{r}:0] q_row [0:3];
200  reg [2:0]  qh, qt;
201  wire       q_full = (qt - qh) == 3'd4;
202  wire       q_some = qt != qh;
203  assign md_valid = tile_valid && !q_full;
204  assign tile_ready = md_ready && !q_full;
205  assign md_data = {{1'b0, tile_line, tile_lines}};
206  wire       tile_take = tile_valid && tile_ready;
207
208  // The head tile: its lines still to take and row bytes still to send, each with its flag (none left, the last beat)
209  // in a register beside it; row bytes sent; the gather register and its bytes; the CRC register; the verdict. The head's
210  // counts are loaded from its queue entry, or from the descriptor taken the clock it becomes the head.
211  reg [{l}:0] lines_left;
212  reg [{b}:0] pay_left;
213  reg         no_lines, final_beat;
214  reg [{b}:0] sent;
215  reg [7:0]   n;
216  reg [1023:0] acc;
217  reg [31:0]  crc;
218  reg         bad;
219  wire        can_emit = q_some && (final_beat ? no_lines : n[7] | n[6]);
220  wire        emit = can_emit && out_ready;
221  wire        shift = emit && !final_beat;
222  wire        retire = emit && final_beat;
223  wire [7:0]  n1 = shift ? n - 8'd64 : n;
224  // Shifting takes 64 from both the gathered bytes and the bytes left, so whether the register holds the rest is told
225  // before the shift; room for a line is told on the bytes after it.
226  wire        holds_rest = {{{pad}'d0, n}} >= pay_left;
227  wire        room = (shift ? n <= 8'd{room_shift} : n <= 8'd{room_hold}) || holds_rest;
228  wire        take = q_some && !no_lines && mr_valid && room;
229  wire        place = take && !holds_rest;
230  wire        last_line = lines_left == {lw}'d1;
231  // The next head's counts: the entry after the head's when it is queued, else the descriptor taken now.
232  wire [2:0]  qn = qh + 3'd1;
233  wire        next_queued = qt != qn;
234  wire [{l}:0] next_lines = next_queued ? q_lines[qn[1:0]] : tile_lines;
235  wire [{b}:0] next_bytes = next_queued ? q_bytes[qn[1:0]] : tile_bytes;
236  wire        load_next = retire && (next_queued || tile_take);
237  wire        load_new = !q_some && tile_take;
238  assign mr_ready = take;
239  wire [1023:0] shifted = shift ? {{512'd0, acc[1023:512]}} : acc;
240  wire [1023:0] line_at = {{{rest}'d0, mr_data}} << {{n1[6:3], 6'd0}};",
241        l = LW - 1,
242        b = BYW - 1,
243        t = TW - 1,
244        r = ROWW - 1,
245        lw = LW,
246        pad = BYW - 8,
247        room_shift = 192 - lb,
248        room_hold = 128 - lb,
249        rest = 1024 - dw
250    );
251    if layout.check {
252        let full = crc_masks(lb as usize);
253        let last = crc_masks((lb - CHECK_BYTES) as usize);
254        let body = 8 * (lb - CHECK_BYTES);
255        let _ = writeln!(v, "  // The CRC register after a whole line, and after a last line's bytes before its check word.");
256        let _ = writeln!(v, "  wire [31:0] crc_full, crc_last;");
257        for (i, (state, data)) in full.iter().enumerate() {
258            let _ = writeln!(v, "  assign crc_full[{i}] = ^(crc & 32'h{state:08x}) ^ ^(mr_data & {dw}'h{});", hex_mask(data));
259        }
260        for (i, (state, data)) in last.iter().enumerate() {
261            let _ = writeln!(
262                v,
263                "  assign crc_last[{i}] = ^(crc & 32'h{state:08x}) ^ ^(mr_data[{}:0] & {body}'h{});",
264                body - 1,
265                hex_mask(data)
266            );
267        }
268        let _ = writeln!(v, "  wire [31:0] check_word = mr_data[{}:{}];", dw - 1, body);
269    }
270    let _ = writeln!(
271        v,
272        "  wire [511:0] keep_mask;
273  genvar i;
274  generate for (i = 0; i < 64; i = i + 1) begin : keep_bytes
275    assign keep_mask[8*i +: 8] = {{8{{i < out_keep}}}};
276  end endgenerate
277  assign out_valid = can_emit;
278  assign out_keep = final_beat ? pay_left[6:0] : 7'd64;
279  assign out_last = final_beat;
280  assign out_data = acc[511:0] & keep_mask;
281  assign out_tensor = q_tensor[qh[1:0]];
282  assign out_row = q_row[qh[1:0]];
283  assign out_offset = sent;
284  assign out_crc_error = {crc_error};
285
286  always @(posedge clk) begin
287    if (!rst_n) begin
288      qh <= 3'd0;
289      qt <= 3'd0;
290      lines_left <= {lw}'d0;
291      pay_left <= {byw}'d0;
292      no_lines <= 1'b1;
293      final_beat <= 1'b1;
294      sent <= {byw}'d0;
295      n <= 8'd0;
296      acc <= 1024'd0;
297      crc <= 32'hffffffff;
298      bad <= 1'b0;
299    end else begin
300      if (tile_take) begin
301        q_lines[qt[1:0]] <= tile_lines;
302        q_bytes[qt[1:0]] <= tile_bytes;
303        q_tensor[qt[1:0]] <= tile_tensor;
304        q_row[qt[1:0]] <= tile_row;
305        qt <= qt + 3'd1;
306      end
307      if (load_next || load_new) begin
308        lines_left <= load_next ? next_lines : tile_lines;
309        pay_left <= load_next ? next_bytes : tile_bytes;
310        no_lines <= (load_next ? next_lines : tile_lines) == {lw}'d0;
311        final_beat <= (load_next ? next_bytes : tile_bytes) <= {byw}'d64;
312      end else begin
313        if (take) begin lines_left <= lines_left - {lw}'d1; no_lines <= last_line; end
314        if (shift) begin pay_left <= pay_left - {byw}'d64; final_beat <= pay_left <= {byw}'d128; end
315      end
316      if (retire) begin
317        qh <= qh + 3'd1;
318        sent <= {byw}'d0;
319        n <= 8'd0;
320        acc <= 1024'd0;
321        crc <= 32'hffffffff;
322        bad <= 1'b0;
323      end else begin
324        if (shift) sent <= sent + {byw}'d64;
325        n <= n1 + (place ? 8'd{lb} : 8'd0);
326        acc <= place ? (shifted | line_at) : shifted;{update}
327      end
328    end
329  end
330endmodule",
331        lw = LW,
332        byw = BYW,
333        lb = lb,
334        crc_error = if layout.check { "final_beat && bad" } else { "1'b0" },
335        update = if layout.check {
336            "\n        if (take) crc <= crc_full;\n        if (take && last_line) bad <= check_word != ~crc_last;"
337        } else {
338            ""
339        }
340    );
341    v
342}