Source code for dau_core.exp_generator

"""Renders the fixed-point exp tile from a declared operating point.

The datapath is :class:`dau_core.fixed_point.ExpDatapath` and the emitted
SystemVerilog mirrors :meth:`ExpDatapath.evaluate` step for step, so the
cocotb bench can compare against it with zero tolerance like every other
tile. The operating point -- internal precision, table size, polynomial
degree, whether Horner steps round -- arrives as configuration, which is why
this is a generator and not a checked-in file: the 256-entry table of
``2**(j/256)`` is a function of those parameters and SystemVerilog cannot
compute it.

The datapath is PIPELINED, one multiply per stage, chosen after measuring
what a multiply costs rather than from the estimate this docstring used to
quote. Measured on the dpv1 Artix (2026-08-09, the registry's points): 50
DSP48s at the exact point and 20 at the light one, against 740 on the part.
Folding onto a shared multiplier
would cut that to roughly a ninth, but at one row per nine cycles against
sqrt's and divide's one per three -- the tile would become the bottleneck of
any chain it joined.
"""

# NOTE: no `from __future__ import annotations` — ccflow's Flow.call
# inspects the real annotation objects on __call__

from ccflow import Flow

from .fixed_point import ExpDatapath, exp_domain
from .generators import CoreGenerator, RenderContext, RenderedCore

__all__ = ("ExpTileGenerator",)


def _hex_literal(value: int, width: int) -> str:
    return f"{width}'h{value & ((1 << width) - 1):0{(width + 3) // 4}x}"


[docs] class ExpTileGenerator(CoreGenerator): """Renders ``dau_fixed_point_exp`` for one operating point.""" module: str = "dau_fixed_point_exp" datapath: ExpDatapath = ExpDatapath() @property def source_name(self) -> str: return f"{self.module}.sv"
[docs] @Flow.call def __call__(self, context: RenderContext) -> RenderedCore: # noqa: ARG002 (ccflow interface) point = self.datapath constants = point.constants() q = point.internal_bits entries = 1 << point.table_bits # the table and the coefficients are just under q+2 bits (values sit # in [1, 2)); the reciprocal of ln2 scaled by the table size is wider wide = q + 2 # the NARROW reciprocal, matching ExpDatapath.evaluate. Emitting the # full-precision one here while the model had moved to the narrow # constant put k out by 2**(internal_bits - k_bits). inv_width = constants["inv_ln2_narrow"].bit_length() + 1 table = ",\n".join(f" {_hex_literal(value, wide)}" for value in constants["table"]) coefficients = ",\n".join(f" {_hex_literal(value, wide)}" for value in constants["coefficients"]) # k reaches about +/-4400 over the useful domain; the extra bits keep # the intermediate shifts clear of it k_width = 20 _low, high = exp_domain(fractional_bits=point.fractional_bits) # A product must be formed at FULL width. In SystemVerilog the # operands of `a * b` are sized by the expression context, so # `WIDE'((acc * res) >>> Q)` evaluates the multiply at WIDE and throws # away the high half BEFORE the shift -- which silently produced zero # for every input. The products therefore get their own wires. # EVERY multiply gets its own pipeline stage. Multiplying, shifting and # adding in one cycle left the DSP48 without its internal register and # measured WNS -4.819 ns at 8 ns on the Artix -- a ~12.8 ns path. The # product is registered, so each Horner step costs two stages. rounding = " + (PROD_ONE <<< (INTERNAL_BITS - 1))" if point.round_steps else "" stages = [] for step in range(point.degree): multiply = 4 + 2 * step accumulate = multiply + 1 coefficient = point.degree - 1 - step stages.append( f" stage_prod[{multiply}] <= PROD_WIDTH'(stage_acc[{multiply - 1}]) * PROD_WIDTH'(stage_res[{multiply - 1}]);" ) stages.append( f" stage_acc[{accumulate}] <= WIDE'(WIDE'(" f"(stage_prod[{multiply}]{rounding}) >>> INTERNAL_BITS)" f" + signed'({{1'b0, COEFFICIENT[{coefficient}]}}));" ) table_multiply = 4 + 2 * point.degree stages.append( f" stage_prod[{table_multiply}] <= PROD_WIDTH'(stage_acc[{table_multiply - 1}])" f" * PROD_WIDTH'(signed'({{1'b0, TABLE[stage_k[{table_multiply - 1}][TABLE_BITS-1:0]]}}));" ) stages.append(f" stage_acc[{table_multiply + 1}] <= WIDE'((stage_prod[{table_multiply}]{rounding}) >>> INTERNAL_BITS);") stages.append(f" stage_acc[{table_multiply + 2}] <= scaled;") horner_body = "\n".join(stages) depth = table_multiply + 3 scale_source = table_multiply + 1 prod_one_decl = " localparam logic signed [PROD_WIDTH-1:0] PROD_ONE = 1;\n" if point.round_steps else "" text = f"""`default_nettype none // GENERATED by dau_core.exp_generator.ExpTileGenerator -- do not edit. // // Fixed-point exp over 16-byte quad rows, rendered for ONE operating point: // internal bits {q}, table {entries} entries, degree {point.degree}, // Horner steps {"rounded" if point.round_steps else "truncated"} // // This tile is generated because its {entries}-entry table of 2**(j/{entries}) and // its polynomial coefficients are transcendental constants at {q} fractional // bits, and SystemVerilog cannot compute those at elaboration. The registry // entry names the generator and the operating point; nothing here is hand // tuned, and editing this file loses the link to the parameters that // produced it. // // Mirrors dau_core.fixed_point.ExpDatapath.evaluate step for step, so the // bench compares with zero tolerance. Deviation from correctly rounded exp // is a MEASURED fact of the operating point, swept over every representable // input and recorded in the registry beside the resource envelope -- not a // tolerance in the bench. module {self.module} #( parameter int INTERNAL_BITS = {q}, parameter int TABLE_BITS = {point.table_bits}, parameter int DEGREE = {point.degree}, /* verilator lint_off UNUSEDPARAM */ // states the operating point in the source; the rounding it selects is // already baked into the emitted Horner steps parameter int ROUND_STEPS = {1 if point.round_steps else 0}, /* verilator lint_on UNUSEDPARAM */ parameter int FRACTIONAL_BITS = {point.fractional_bits} ) ( input wire logic clk, input wire logic rst, input wire logic [1:0] cfg_dst_field, input wire logic [1:0] cfg_src_field, input wire logic input_valid, output logic input_ready, input wire logic [63:0] input_data, input wire logic input_last, output logic output_valid, input wire logic output_ready, output logic [63:0] output_data, output logic output_last, output logic status_valid, input wire logic status_ready, output logic status_error, output logic [7:0] status_error_code ); localparam int WIDE = {wide}; localparam int INV_WIDTH = {inv_width}; localparam int K_WIDTH = {k_width}; localparam int K_BITS = {point.k_bits}; localparam int TABLE_ENTRIES = {entries}; localparam logic [INV_WIDTH-1:0] INV_LN2 = {_hex_literal(constants["inv_ln2_narrow"], inv_width)}; localparam logic [WIDE-1:0] LN2 = {_hex_literal(constants["ln2"], wide)}; localparam logic [WIDE-1:0] TABLE [TABLE_ENTRIES] = '{{ {table} }}; localparam logic [WIDE-1:0] COEFFICIENT [DEGREE+1] = '{{ {coefficients} }}; localparam logic [7:0] ERR_STREAM = 8'd2; localparam logic [7:0] ERR_RANGE = 8'd4; // exp of anything above this does not fit int32; below the low bound the // correctly rounded answer is 0, which the final shift produces on its // own -- an underflow is an answer, not an error. localparam logic signed [31:0] MAX_INPUT = 32'sd{high}; // the datapath, one multiply per stage: // 0 push 1 k 2 shifted ln2 product 3 residue, then a MULTIPLY stage and an ACCUMULATE // stage per Horner step, the table multiply, and the final scale localparam int DEPTH = {depth}; typedef enum logic [1:0] {{ IN_W0, IN_W1, IN_DISCARD }} in_state_t; typedef enum logic [1:0] {{ OUT_IDLE, OUT_W0, OUT_W1 }} out_state_t; in_state_t in_state; out_state_t out_state; logic batch_open; logic [1:0] active_dst; logic [1:0] active_src; logic status_pending; logic [7:0] pending_error; logic [31:0] held_field0; logic [31:0] held_field1; logic stage_valid [DEPTH]; logic signed [WIDE-1:0] stage_acc [DEPTH]; logic signed [WIDE-1:0] stage_res [DEPTH]; logic signed [K_WIDTH-1:0] stage_k [DEPTH]; logic [31:0] stage_field [DEPTH][4]; logic [1:0] stage_dst [DEPTH]; logic stage_last [DEPTH]; logic [31:0] emit_field [4]; logic emit_last; logic input_fire; logic output_fire; logic retire_valid; logic pipe_advance; logic pipe_push; logic [31:0] in_field [4]; logic signed [31:0] src_value; logic range_error; assign in_field[0] = held_field0; assign in_field[1] = held_field1; assign in_field[2] = input_data[31:0]; assign in_field[3] = input_data[63:32]; assign src_value = signed'(in_field[active_src]); assign range_error = (src_value > MAX_INPUT); assign input_fire = input_valid && input_ready; assign output_fire = output_valid && output_ready; assign retire_valid = stage_valid[DEPTH-1]; assign pipe_advance = !retire_valid || (out_state == OUT_IDLE); assign input_ready = !status_pending && ((in_state == IN_W0) || (in_state == IN_DISCARD) || ((in_state == IN_W1) && pipe_advance)); assign pipe_push = (in_state == IN_W1) && input_fire && !range_error; assign output_valid = (out_state == OUT_W0) || (out_state == OUT_W1); assign output_data = (out_state == OUT_W0) ? {{emit_field[1], emit_field[0]}} : {{emit_field[3], emit_field[2]}}; assign output_last = (out_state == OUT_W1) && emit_last; assign status_valid = status_pending; assign status_error = 1'b1; assign status_error_code = pending_error; // Products are formed at full width on purpose; see the generator. localparam int PROD_WIDTH = 2 * WIDE; {prod_one_decl} logic signed [PROD_WIDTH-1:0] stage_prod [DEPTH]; logic signed [RES_WIDTH-1:0] stage_ln2 [DEPTH]; // Stages read the x CARRIED in the pipeline, never the live input // port. Reading src_value here worked for a single parked row and // produced garbage the moment two rows were in flight, because each // stage then saw a different row's operand. // stage 1: the reduction index, from a NARROW constant on purpose localparam int KP_WIDTH = 32 + INV_WIDTH; logic signed [KP_WIDTH-1:0] k_product; assign k_product = KP_WIDTH'(stage_res[0]) * KP_WIDTH'(signed'({{1'b0, INV_LN2}})); // stage 2: the residue, x scaled up minus k units of ln2/TABLE_ENTRIES localparam int RES_WIDTH = WIDE + K_WIDTH + 1; logic signed [RES_WIDTH-1:0] ln2_product; // the residue is bounded by ln2/(2*TABLE_ENTRIES), so it fits WIDE by // construction and the high bits are deliberately dropped /* verilator lint_off UNUSEDSIGNAL */ logic signed [RES_WIDTH-1:0] residue_full; /* verilator lint_on UNUSEDSIGNAL */ assign ln2_product = RES_WIDTH'(stage_k[1]) * RES_WIDTH'(signed'({{1'b0, LN2}})); assign residue_full = (RES_WIDTH'(stage_res[2]) <<< (INTERNAL_BITS - FRACTIONAL_BITS)) - stage_ln2[2]; // the final scale: 2**e comes out as a shift, e = k >>> TABLE_BITS logic signed [K_WIDTH-1:0] final_e; logic [7:0] final_shift; logic signed [WIDE-1:0] scaled; assign final_e = stage_k[{scale_source}] >>> TABLE_BITS; assign final_shift = 8'((INTERNAL_BITS - FRACTIONAL_BITS) - final_e); // The rounding term must be added in a WIDER type: at the bottom of the // domain the shift reaches WIDE-1, and acc + (1 << (WIDE-2)) overflows a // signed WIDE, which wrapped a small positive answer to -1. localparam int SCALE_WIDTH = WIDE + 2; logic signed [SCALE_WIDTH-1:0] scale_sum; assign scale_sum = SCALE_WIDTH'(stage_acc[{scale_source}]) + (SCALE_WIDTH'(signed'(1)) <<< (final_shift - 8'd1)); // beyond SCALE_WIDTH the rounded result is zero, which is the correctly // rounded answer for an underflow, not an error assign scaled = (final_shift >= 8'(SCALE_WIDTH)) ? WIDE'(signed'(0)) : WIDE'(scale_sum >>> final_shift); always_ff @(posedge clk) begin if (rst) begin in_state <= IN_W0; out_state <= OUT_IDLE; batch_open <= 1'b0; status_pending <= 1'b0; pending_error <= 8'd0; emit_last <= 1'b0; for (int unsigned i = 0; i < DEPTH; i++) stage_valid[i] <= 1'b0; end else begin if (pipe_advance) begin stage_valid[0] <= pipe_push; if (pipe_push) begin stage_last[0] <= input_last; stage_dst[0] <= active_dst; stage_acc[0] <= WIDE'(signed'({{1'b0, COEFFICIENT[DEGREE]}})); stage_res[0] <= WIDE'(src_value); // carries x until the residue exists stage_k[0] <= '0; for (int unsigned f = 0; f < 4; f++) stage_field[0][f] <= in_field[f]; end for (int unsigned i = 1; i < DEPTH; i++) begin stage_valid[i] <= stage_valid[i-1]; stage_last[i] <= stage_last[i-1]; stage_dst[i] <= stage_dst[i-1]; stage_k[i] <= stage_k[i-1]; stage_acc[i] <= stage_acc[i-1]; stage_prod[i] <= stage_prod[i-1]; stage_res[i] <= stage_res[i-1]; for (int unsigned f = 0; f < 4; f++) stage_field[i][f] <= stage_field[i-1][f]; end // stage 1 overrides: k // truncated, not rounded: the rounding add spanned the whole // product and measured as a 13-deep CARRY4 chain between two // DSPs, which was the tile's critical path stage_k[1] <= K_WIDTH'(k_product >>> (K_BITS + FRACTIONAL_BITS)); // stage 2 overrides: the residue replaces the carried x // the shifted product is registered, so the subtract no longer // sits combinationally between two DSP48s (clock-to-out ~3.5 ns // leaves no room for a carry chain at 8 ns) stage_ln2[2] <= ln2_product >>> TABLE_BITS; stage_res[3] <= WIDE'(residue_full); {horner_body} // the table multiply, then the scale end case (in_state) IN_W0: if (input_fire) begin if (input_last) begin pending_error <= ERR_STREAM; status_pending <= 1'b1; end else begin held_field0 <= input_data[31:0]; held_field1 <= input_data[63:32]; if (!batch_open) begin batch_open <= 1'b1; active_dst <= cfg_dst_field; active_src <= cfg_src_field; end in_state <= IN_W1; end end IN_W1: if (input_fire) begin if (range_error) begin pending_error <= ERR_RANGE; if (input_last) status_pending <= 1'b1; else in_state <= IN_DISCARD; end else begin in_state <= IN_W0; if (input_last) batch_open <= 1'b0; end end IN_DISCARD: if (input_fire && input_last) status_pending <= 1'b1; default: in_state <= IN_W0; endcase case (out_state) OUT_IDLE: if (retire_valid) begin for (int unsigned f = 0; f < 4; f++) emit_field[f] <= stage_field[DEPTH-1][f]; emit_field[stage_dst[DEPTH-1]] <= 32'(stage_acc[DEPTH-1]); emit_last <= stage_last[DEPTH-1]; out_state <= OUT_W0; end OUT_W0: if (output_fire) out_state <= OUT_W1; OUT_W1: if (output_fire) out_state <= OUT_IDLE; default: out_state <= OUT_IDLE; endcase if (status_pending && status_ready) begin status_pending <= 1'b0; in_state <= IN_W0; batch_open <= 1'b0; for (int unsigned i = 0; i < DEPTH; i++) stage_valid[i] <= 1'b0; end end end endmodule `default_nettype wire """ return RenderedCore( module=self.module, source_name=self.source_name, text=text, parameters={ "INTERNAL_BITS": q, "TABLE_BITS": point.table_bits, "DEGREE": point.degree, "ROUND_STEPS": int(point.round_steps), "FRACTIONAL_BITS": point.fractional_bits, }, )