Skip to content
VLSI Mentor

Ethernet · Module 23

SmartNICs

645 instructions per packet at 64 cores, 197× the area of the fixed-function blocks it replaces, and a behaviour that is decided when somebody loads a program.

Chapter 23.4 built a NIC whose behaviour was fixed at tape-out. This chapter makes it programmable, and the three quantities that change are an instruction budget, an area, and the moment the device's behaviour is decided.

At 100 Gb/s, minimum frames — 148.81 MppsValue
8 packet cores at 1.5 GHz, 1 instruction per cycle80.6 instructions per packet
16 cores161.3
64 cores645.1
64 cores at 2.0 GHz and 2 IPC1 720.3

Row three is the figure to carry. A 64-core packet-processing array running at 1.5 GHz gives 645 instructions per packet — enough for a firewall, a tunnel encapsulation, a flow table lookup and a rewrite, and nowhere near enough for a full transport implementation.

Set against the host it is offloading.

Instructions per packet
one 3 GHz host core at 3 instructions per cycle60.5
64 SmartNIC cores at 1.5 GHz645.1
host cores needed to match10.7

So a 64-core array is worth about eleven host cores of packet-processing work — which is the entire commercial argument for the part, and it is an argument about the host's cost rather than the NIC's.

Then the area, in Chapter 23.3 §2's unit.

BCE× a MAC receive datapath
Chapter 18.7 §18's fixed-function offload set — 5 650 flops1.13 × 10⁵0.40
64 packet cores at 30 kGE each5.49 × 10⁶19.4
64 × 32 kB of instruction memory1.68 × 10⁷59.2
the programmable datapath2.23 × 10⁷78.6
ratio against the fixed-function set197×

A programmable datapath costs 197 times the fixed-function blocks it replaces, and makes Chapter 23.4's NIC 44% larger.

And the third change is not a number at all.

A fixed-function NICA SmartNIC
when its behaviour is decidedat tape-outwhen a program is loaded
what a property quantifies overone designan open set of programs
what was proved at verificationthe device's behaviourthe behaviour of the programs that existed then
what enforces the proof's domainthe siliconnothing, unless something is added

Row four is Section 20's refused property, and Section 11 is the mechanism — a verifier in the load path — that turns an open quantification back into a closed one.


1. Scope — What "Programmable" Costs, in Instructions and in Area

Scope: what work moves onto a programmable NIC, what the per-packet instruction budget is, what the programmability costs in area, and what it does to verification.

Not in scope: the transport. Whether a SmartNIC terminates TCP, implements RDMA or runs a virtual switch is a product decision; this chapter prices the capacity to do such things and leaves the choice of which to somebody else.

Three questions, and the third is the one the track has not met before.

QuestionSection
1how much computation is available per packet?2 and 4
2what does the programmability cost against fixed function?6 and 19
3what happens to a proof when the behaviour arrives later?10, 12 and 20

Question 3 is new to the track. Every earlier chapter verified a design — a MAC, a PHY, a switch pipeline, a NIC — whose behaviour was fully determined by the silicon. A SmartNIC's behaviour is determined by silicon plus a program, and the program is loaded by an operator, after the part shipped, from a set that did not exist when the part was verified.

And one framing that runs through the chapter.

A fixed-function block is a program that was compiled into silicon. The difference is not what it computes — it is when the computation was chosen, and therefore what could be proved about it.

Which makes the comparison in Section 17 a three-way one rather than a two-way one.

Fixed functionProgrammable datapathHost software
instructions per packeteffectively unbounded, in parallel645 at 64 cores60.5 per core
area0.40 datapaths78.6none on the NIC
behaviour decidedat tape-outat program loadat every deployment
what can be provedeverythingwhat the verifier enforcesnothing, by the NIC
changeable after shippingnoyesyes

Rows four and five are the trade, and they are the same trade every programmable system makes. What this chapter adds is the price in Chapter 23.3 §2's unit and the per-packet budget, both of which are usually asserted rather than derived.


2. The Per-Packet Instruction Budget

A programmable network interface card's architecture follows from a single division: instructions per packet equals cores times frequency times instructions per cycle, divided by packets per second. At one hundred gigabits per second with minimum size frames the packet rate is one hundred and forty eight point eight one million per second. Eight cores at one point five gigahertz with one instruction per cycle therefore give eighty point six instructions per packet, sixteen cores give one hundred and sixty one point three, thirty two give three hundred and twenty two point six, and sixty four give six hundred and forty five point one. Sixty four cores at two gigahertz with two instructions per cycle give one thousand seven hundred and twenty point three. Read the other way, which is how a design is actually sized, a fifty instruction program needs five cores, a hundred instruction program needs nine point nine, five hundred needs forty nine point six, one thousand needs ninety nine point two, and two thousand needs one hundred and ninety eight point four cores, which is two hundred and forty three media access control receive datapaths of area for one program's worth of work. For comparison a single three gigahertz host core at three instructions per cycle manages sixty point five instructions per packet, so the sixty four core array is worth ten point seven host cores, and that comparison is the entire commercial argument for the part. What fits in six hundred and forty five instructions is header work: parsing takes forty to eighty, a table lookup and action thirty to sixty, a tunnel encapsulation fifty to a hundred, and a stateful firewall's connection update eighty to a hundred and fifty, so all four together are two hundred to three hundred and ninety with margin to spare. What does not fit is anything per octet.148.81 Mpps100 Gb/s, 64-octetframes8 cores80.6 instr/packet64 cores at 1.5GHz645.1 instr/packetOne host core60.5 instr/packetHeader work fits200–390 for fourfunctions10.7 host coresthe commercial casePer-octet doesnotAES 1 500 B = 750instr2 000 instr = 198cores243 datapaths12
Figure 1 — one division sets the architecture, and reading it backwards sizes the array.

One division sets the whole architecture.

instructions per packet = cores × frequency × IPC ÷ packets per second

At 148.81 Mpps.

CoresFrequencyIPCInstructions per packet
81.5 GHz180.6
161.5 GHz1161.3
321.5 GHz1322.6
641.5 GHz1645.1
642.0 GHz21 720.3

And read the other way, which is how a design is actually sized.

Instructions per packet requiredCores at 1.5 GHz
505.0
1009.9
50049.6
1 00099.2
2 000198.4

Row five is where the approach stops being viable, and it is worth saying why in area terms rather than in principle: 198 cores at 30 kGE each is 1.70 × 10⁷ BCE of logic plus 5.19 × 10⁷ of instruction memory — 243 MAC receive datapaths — which is a large fraction of Chapter 23.4's entire NIC for one program's worth of work.

What fits in the budget, at 645 instructions.

TaskRough instruction countFits?
parse Ethernet, VLAN, IPv4, TCP40–80yes
a flow-table lookup and action30–60yes
VXLAN or GENEVE encapsulation50–100yes
a stateful firewall's connection update80–150yes
all four together200–390yes, with margin
a full TCP state machine per segment1 000–3 000no
AES-GCM over a 1 500-octet payloadthousandsno — needs a fixed-function engine

Rows six and seven are where a SmartNIC stops being a general-purpose processor and starts being a processor with accelerators bolted to it, which is what every real one is. The programmable cores do the decisions and fixed-function engines do the bulk arithmetic — a checksum, a cipher, a hash — because the instruction budget can afford a decision and cannot afford a loop over the payload.

The budget is per packet, not per octet. Anything that touches every byte must be fixed function, and anything that touches a header can be a program.

One number makes that concrete. AES-GCM over a 1 500-octet payload at even two octets per instruction is 750 instructionsmore than the entire 64-core budget for one packet — so a design that encrypts in software sustains at most 128 Mpps of otherwise-zero processing, and in practice far less. A fixed-function AES engine does it at one packet per cycle for a few hundred thousand BCE.


3. RTL 1 — The SmartNIC Package and the Instruction Budget

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// smartnic_pkg -- a programmable NIC, and the one division that sets
// its architecture.
//
// Chapter 23.3's package held an area model; Chapter 23.4's held a
// bandwidth model. This one holds a COMPUTE model, because the
// quantity a SmartNIC rations is instructions per packet -- and
// Section 10 shows the quantity it cannot ration at all is which
// instructions.
// ---------------------------------------------------------------------
package smartnic_pkg;

  // ---- the wire, unchanged from Chapter 23.4 -----------------------------
  localparam int RATE_GBPS    = 100;
  localparam int PPS_MILLION  = 148;

  // ---- the compute array ------------------------------------------------
  localparam int CORES        = 64;
  localparam int CORE_MHZ     = 1500;
  localparam int IPC          = 1;
  localparam int IMEM_KB      = 32;
  localparam int CORE_GE      = 30000;

  // ---- the area unit, from Chapter 23.3 Section 2 ------------------------
  localparam int BCE_PER_FLOP = 20;
  // 1 GE = 1 / 0.35 BCE, expressed as a scaled integer.
  localparam int BCE_PER_GE_X100 = 286;
  localparam int DATAPATH_BCE = 283320;

  // ---- the budget --------------------------------------------------------
  function automatic int instr_per_packet();
    return (CORES * CORE_MHZ * IPC * 1000) / PPS_MILLION;
  endfunction

  function automatic int cores_for(int instr);
    return (instr * PPS_MILLION) / (CORE_MHZ * 1000 * IPC);
  endfunction

  // A host core, for the comparison that justifies the part.
  localparam int HOST_MHZ = 3000;
  localparam int HOST_IPC = 3;
  function automatic int host_instr_per_packet();
    return (HOST_MHZ * HOST_IPC * 1000) / PPS_MILLION;
  endfunction

  // ---- area --------------------------------------------------------------
  function automatic int array_bce();
    return (CORES * CORE_GE * BCE_PER_GE_X100) / 100
         + (CORES * IMEM_KB * 1024 * 8);
  endfunction

  // Chapter 18.7 Section 18's fixed-function offload set, for the ratio.
  localparam int FIXED_OFFLOAD_FLOPS = 5650;
  localparam int FIXED_OFFLOAD_BCE   = FIXED_OFFLOAD_FLOPS * BCE_PER_FLOP;

  // ---- what a program is --------------------------------------------------
  // The device's behaviour is a function of THIS, and it arrives after
  // the silicon does.
  typedef struct packed {
    logic [31:0] program_id;
    logic [15:0] instruction_count;
    logic [15:0] worst_case_instructions;   // often unknown
    logic [31:0] signature;
    logic        bounded_loops;             // a verifier's verdict
    logic        verified;
  } program_t;

  // Section 12: what a load-time verifier can and cannot establish.
  typedef enum logic [2:0] {
    VER_NONE        = 3'd0,
    VER_SIGNATURE   = 3'd1,   // it came from somebody we trust
    VER_TERMINATION = 3'd2,   // it halts
    VER_BUDGET      = 3'd3,   // it halts within N instructions
    VER_MEMORY      = 3'd4,   // it touches only what it may
    VER_FULL        = 3'd5    // all of the above
  } verification_level_e;

endpackage

Classification: a compute model, and its last declaration is a list of things a load-time check can establish.

What it teaches: that program_t.worst_case_instructions is a field that is often unknown and is the one the design depends on. An array sized for 645 instructions per packet sustains line rate only if every packet's program path fits in 645; a program whose worst case is 1 200 sustains 80 Mpps on the traffic that triggers it — Section 13's arithmetic. The mean is irrelevant and the worst case is frequently not computed, because computing it is a static-analysis problem the program's author had no reason to solve.

And it teaches that verification_level_e is a ladder with a top rung that is reachable. A loaded program can be checked for a signature (cheap, and establishes provenance rather than behaviour), termination, an instruction budget, and memory safety — and the last three are decidable for the restricted languages real SmartNICs use. That is the mechanism that closes Section 20's open quantification, and it lives in the load path rather than in a property.

Deliberately simplified: IPC = 1 where real packet cores are often multi-issue or multi-threaded, and a multi-threaded core hides memory latency in a way this model cannot express. CORE_GE = 30000 is a plausible figure for a small RISC core with a packet-oriented instruction set and varies by a factor of three across real designs. instr_per_packet assumes every core is available to every packet, ignoring the scheduler and any per-core specialisation. And program_t has no notion of several programs loaded at once, which every real device supports and which multiplies the verification problem.

Production implication: the missing multi-program case is where the load-time verifier's job becomes genuinely hard, and it is the normal deployment. Two programs that are each individually within budget can exceed it together — 400 instructions plus 400 is 800 against 645 — and two programs that are each memory-safe can interfere if they share a table. So the verifier's verdict cannot be per program; it must be over the loaded set, re-evaluated on every load and unload, which makes it a global check at a moment when the device is running at line rate. The practical answer is to verify against a static budget allocation per program — each program declares its share and the verifier checks the sum — which is weaker than verifying the composition and is the only thing that is affordable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// instruction_budget -- the division of Section 2, as a block, with the
// worst case separated from the mean.
//
// The block exists because a SmartNIC's throughput is set by the WORST
// path through the loaded program and every measurement people take
// reports the mean.
// ---------------------------------------------------------------------
module instruction_budget
  import smartnic_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic [15:0] pps_million,
  input  logic [15:0] cores,
  input  logic [15:0] core_mhz,
  input  logic [7:0]  ipc,
  input  logic [15:0] program_mean_instr,
  input  logic [15:0] program_worst_instr,

  output logic [31:0] budget_per_packet,
  output logic [15:0] sustained_mpps_mean,
  output logic [15:0] sustained_mpps_worst,
  output logic [15:0] worst_case_pct_of_line,
  output logic        fits_at_mean,
  output logic        fits_at_worst,
  output logic [15:0] cores_needed_worst,
  output logic [15:0] host_cores_equivalent
);
  logic [31:0] array_mips;

  always_comb begin
    array_mips = 32'(cores) * 32'(core_mhz) * 32'(ipc);   // millions/s

    budget_per_packet = (pps_million == 0) ? 32'hFFFF_FFFF
                      : (array_mips / 32'(pps_million));

    sustained_mpps_mean = (program_mean_instr == 0) ? 16'hFFFF
                        : 16'(array_mips / 32'(program_mean_instr));
    sustained_mpps_worst = (program_worst_instr == 0) ? 16'hFFFF
                         : 16'(array_mips / 32'(program_worst_instr));

    worst_case_pct_of_line = (pps_million == 0) ? 16'd0
                           : (sustained_mpps_worst * 16'd100) / pps_million;

    fits_at_mean  = (sustained_mpps_mean  >= pps_million);
    fits_at_worst = (sustained_mpps_worst >= pps_million);

    cores_needed_worst = 16'((32'(program_worst_instr) * 32'(pps_million))
                             / (32'(core_mhz) * 32'(ipc)));

    // The commercial argument: how many host cores this array replaces.
    host_cores_equivalent = 16'(array_mips
                                / (32'(HOST_MHZ) * 32'(HOST_IPC)));
  end
endmodule

Classification: a division, computed twice — once with the mean and once with the worst case.

What it teaches: that fits_at_mean and fits_at_worst can differ, and only the second matters. A program averaging 400 instructions on a 64-core array fits comfortably at the mean — 240 Mpps against a required 148.81 — and a worst case of 1 200 gives 80 Mpps, 53.8% of line rate. The part is at line rate on ordinary traffic and at half on traffic that takes the long path, and which traffic that is depends on packet contents.

And it teaches that host_cores_equivalent is the number that sells the part. A 64-core array at 1.5 GHz executes 96 000 million instructions per second; a 3 GHz host core at 3 IPC executes 9 000so the array is worth 10.7 host cores. That comparison is the commercial case and it is independent of the packet rate, which is why it survives every workload argument.

Deliberately simplified: the model assumes perfect load balancing across cores, which a real scheduler approximates; a run-to-completion model with packets pinned to cores has the same balls-in-bins problem Chapter 23.2 §10 derived for uplinks. program_worst_instr is an input that a static analyser must supply and frequently cannot. There is no memory-latency term, so a program that misses a table lookup stalls in a way the model does not represent. And host_cores_equivalent compares raw instruction throughput, ignoring that a host core's instructions are wider and its memory system faster.

Production implication: the perfect-load-balancing assumption is the one that turns an adequate array into an inadequate one, and the mechanism is familiar. A run-to-completion architecture assigns a packet to a core and keeps it there for the packet's whole program, so the cores are bins and the packets are balls — and Chapter 23.2 §10's arithmetic applies exactly. With 64 cores and packets arriving at 148.81 Mpps, a long-running packet occupies its core for the whole of its program, and a burst of long-path packets can leave most of the array idle while a few cores are saturated. The remedy is the same one that chapter found: more, smaller units of work — split the program into stages and pipeline them across cores rather than running each packet to completion — which trades per-packet locality for utilisation.


4. Six Hundred and Forty-Five Instructions, and What Fits In Them

Section 2 gave the budget. This section spends it, and the spending pattern explains the architecture of every real SmartNIC.

What a program does per packet, by category.

CategoryInstructionsTouches
header parsing40–80the first 64 octets
table lookups and actions30–60 eacha key and a result
header rewriting20–50the first 64 octets
stateful updates80–150a per-flow record
payload transformationthousandsevery octet

Rows one to four are all header work and they all fit. A realistic composition — parse, two lookups, a rewrite and a connection update — is 200 to 390 instructions, comfortably inside 645. Row five does not fit and never will, because its cost scales with the payload and the budget does not.

The arithmetic that makes row five hopeless.

TransformationOctets touchedAt 2 octets per instructionAgainst a 645 budget
a 64-octet frame6432 instructionsfits
a 512-octet frame512256fits, barely
a 1 500-octet frame1 500750does not fit
a 9 000-octet frame9 0004 5007× over

And the consequence is the structure every SmartNIC has.

The programmable cores make decisions about headers; fixed-function engines transform payloads. The budget is per packet and a payload is per octet, so the two cannot be done by the same mechanism.

Which gives the part's real shape.

MechanismCost
parse, classify, decide, rewritethe programmable array78.6 datapaths
checksumChapter 18.7 §3's engine0.03 datapaths
hashChapter 18.7 §8's Toeplitz tree0.02 datapaths
ciphera fixed AES-GCM enginea few hundred thousand BCE
compressiona fixed engine, if present at alllarger still

Row one costs more than every other row combined by two orders of magnitude, and it is the row that can be changed after shipping. That is the entire trade: 197× the area of the fixed-function set, for the ability to decide what the part does after it is in a rack.

One qualification about the instruction counts. The figures above are for a packet-oriented instruction set — one with fields addressed by name, table lookups as single instructions and no general-purpose addressing. A general-purpose ISA doing the same work needs two to four times as many instructions, which is why SmartNIC cores are not simply small ARM or RISC-V cores in most designs, and why the ones that are need correspondingly more of them.


5. RTL 2 — The Core Array

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// core_array -- 64 packet cores, a scheduler, and the balls-in-bins
// problem that Chapter 23.2 Section 10 derived for uplinks arriving
// here for cores.
//
// A run-to-completion array assigns a packet to a core and keeps it
// there for the whole program. Cores are bins, packets are balls, and
// a long-running packet holds its bin.
// ---------------------------------------------------------------------
module core_array
  import smartnic_pkg::*;
#(
  parameter int N = CORES              // 64
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        pkt_arrives,
  input  logic [15:0] pkt_instructions,      // this packet's path length
  input  logic        run_to_completion,

  output logic        accepted,
  output logic [6:0]  assigned_core,
  output logic [6:0]  cores_busy,
  output logic [15:0] core_remaining [N],
  output logic [15:0] utilisation_pct,
  output logic        array_saturated,
  output logic [31:0] c_accepted,
  output logic [31:0] c_rejected,
  output logic [15:0] longest_occupancy
);
  logic [6:0] free_core;
  logic       any_free;

  always_comb begin
    any_free  = 1'b0;
    free_core = 7'd0;
    cores_busy = 7'd0;
    longest_occupancy = 16'd0;
    for (int i = 0; i < N; i++) begin
      if (core_remaining[i] == 16'd0) begin
        if (!any_free) begin any_free = 1'b1; free_core = 7'(i); end
      end else begin
        cores_busy = cores_busy + 7'd1;
        if (core_remaining[i] > longest_occupancy)
          longest_occupancy = core_remaining[i];
      end
    end

    accepted      = pkt_arrives && any_free;
    assigned_core = free_core;
    array_saturated = pkt_arrives && !any_free;

    utilisation_pct = 16'((32'(cores_busy) * 100) / N);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_accepted <= '0; c_rejected <= '0;
      for (int i = 0; i < N; i++) core_remaining[i] <= '0;
    end else begin
      for (int i = 0; i < N; i++)
        if (core_remaining[i] != 16'd0)
          core_remaining[i] <= core_remaining[i] - 16'd1;

      if (accepted) begin
        // Run to completion: the core is held for the WHOLE program,
        // so a long path holds a core for a long time.
        core_remaining[assigned_core] <= run_to_completion
                                       ? pkt_instructions
                                       : 16'd1;    // pipelined: one stage
        c_accepted <= c_accepted + 32'd1;
      end else if (pkt_arrives) begin
        c_rejected <= c_rejected + 32'd1;
      end
    end
  end
endmodule

Classification: a run-to-completion scheduler, and its failure mode is Chapter 23.2 §10's in a new setting.

What it teaches: that run_to_completion chooses between two completely different utilisation behaviours. With it true, a packet holds a core for its whole program — so a burst of 1 200-instruction packets occupies 64 cores for 800 nanoseconds and rejects everything that arrives meanwhile. With it false, the array is a pipeline: each core does one stage, packets flow through, and a long packet occupies each stage for one cycle. The second utilises better and loses per-packet locality, which is why a program with a large working set prefers the first.

And it teaches that longest_occupancy is the quantity that predicts rejection. The array saturates not when it is busy on average but when every core is simultaneously held, and a run-to-completion array with variable path lengths has exactly Chapter 23.2 §10's balls-in-bins distribution — a few cores holding long packets while the rest idle, and arrivals rejected because the free ones happen to be the wrong ones.

Deliberately simplified: the free-core search is a linear scan over 64 entries, combinational, which at 148.81 Mpps is a 64-way priority encoder and is fine. core_remaining decrements once per clock for every core, modelling one instruction per cycle with no stalls — a table lookup that misses stalls for hundreds of cycles and the model has no way to say so. run_to_completion false models a pipeline as a single stage. And a rejected packet is simply counted, where a real design buffers it, punts it or drops it, and the choice matters.

Production implication: the missing memory-stall term is what makes a real array's utilisation far worse than this model's, and it is the reason packet cores are multi-threaded. A program that performs three table lookups, each missing a local cache and costing 200 cycles, spends 600 of its budget stalled — and a single-threaded core is idle for all of it. Four-way multi-threading lets three other packets use those cycles, which raises utilisation roughly fourfold for the same core count and is why real packet cores have several hardware threads and a small register file per thread. The model's IPC = 1 is best read as one instruction per thread-cycle, and a design that sizes an array from it without threading needs four times the cores.


6. What a Programmable Datapath Costs Against the Blocks It Replaces

Pricing a programmable array against the fixed function blocks it replaces, in the bitcell equivalent unit defined in chapter twenty three point three. The fixed function offload set from chapter eighteen point seven is five thousand six hundred and fifty flip flops, which is one hundred and thirteen thousand bitcell equivalents, or zero point four zero of a media access control receive datapath, and it comprises a receive checksum engine, a transmit checksum inserter, a segmentation offload engine, a receive side scaling hash, an indirection table and a queue set. The programmable array that replaces the first five of those is sixty four cores at thirty thousand gate equivalents each, which at one gate equivalent to two point eight six bitcell equivalents is five point four nine million, or nineteen point four datapaths, plus sixty four instruction memories of thirty two kilobytes each, which is sixteen point eight million bitcell equivalents or fifty nine point two datapaths. The array total is twenty two point three million bitcell equivalents, seventy eight point six datapaths, and one hundred and ninety seven times the fixed function set. Two thirds of that cost is instruction memory rather than cores, so reducing the per core instruction memory from thirty two kilobytes to eight saves forty four point four datapaths, which is fifty seven per cent of the array and the largest single lever available. Adding the array to chapter twenty three point four's fixed function card takes it from five point zero seven times ten to the seventh bitcell equivalents to seven point three zero, an increase of forty three point nine per cent, and every bit of that increase buys the same thing: the ability to decide what the part does after it is in a rack.18.7's offloadset5 650 flops — 0.40datapaths64 cores5.49e6 BCE — 19.464 × 32 kB imem1.68e7 BCE — 59.2The array2.23e7 BCE — 78.6197×against the fixed set8 kB instead of32saves 57% of the arrayNIC +43.9%179 → 257.6 datapaths12
Figure 2 — 197 times the area, and two thirds of it is somewhere to keep the programs.

Chapter 18.7 §18 priced a fixed-function offload set. This section prices the programmable array that would do the same work, in Chapter 23.3 §2's unit.

The fixed-function set.

BlockFlopsBCE
receive checksum engine4008 000
transmit checksum inserter4509 000
segmentation offload2505 000
RSS hash3507 000
RSS indirection table70014 000
queue set, 16 queues2 60052 000
telemetry and conformance90018 000
total — Chapter 18.7 §185 650113 000 — 0.40 datapaths

The programmable array that replaces the first five rows.

QuantityBCEDatapaths
64 cores at 30 kGE each1.92 × 10⁶ GE5.49 × 10⁶19.4
64 × 32 kB instruction memory1.68 × 10⁷ bits1.68 × 10⁷59.2
total2.23 × 10⁷78.6

The ratio is 197.

A programmable datapath costs 197 times the fixed-function blocks it replaces, and two thirds of that cost is instruction memory rather than cores.

Row two of the second table is the one people forget. The cores are 19.4 datapaths; the memory to hold their programs is 59.2three times more. A design that reduces the instruction memory from 32 kB to 8 kB per core saves 44.4 datapaths, 57% of the array, at the cost of program size.

Instruction memory per coreTotalDatapathsArray total
8 kB4.19 × 10⁶ BCE14.834.2
16 kB8.39 × 10⁶29.649.0
32 kB1.68 × 10⁷59.278.6
64 kB3.36 × 10⁷118.4137.8

And against Chapter 23.4's NIC.

BCEDatapaths
Chapter 23.4 §10's fixed-function NIC5.07 × 10⁷179.0
plus a 64-core array at 32 kB7.30 × 10⁷257.6
increase+2.23 × 10⁷+43.9%

A SmartNIC is about 44% larger than the fixed-function NIC it is built from, and every bit of that increase buys the same thing: the ability to decide what the part does after it ships.

One comparison that puts the ratio in perspective.

Datapaths
Chapter 18.7's whole offload set0.40
Chapter 23.3's entire switch pipeline logic0.84
a 64-core programmable array78.6

The array is ninety-four times the forwarding logic of a 6.4 Tb/s switch. That is not an argument against it — a switch's pipeline does one fixed thing very fast and the array does an arbitrary thing rather fastbut it is the number to have when somebody describes programmability as cheap.


7. RTL 3 — The Fixed-Versus-Programmable Comparator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// offload_comparator -- the same function, two ways, priced in the
// same unit.
//
// The block exists so that "should this be fixed function or a
// program" is answered with two numbers rather than a preference.
// ---------------------------------------------------------------------
module offload_comparator
  import smartnic_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic [15:0] fixed_flops,
  input  logic [15:0] program_instructions,
  input  logic [15:0] pps_million,
  input  logic        touches_every_octet,
  input  logic [15:0] expected_changes_per_year,

  output logic [31:0] fixed_bce,
  output logic [31:0] prog_bce,
  output logic [15:0] ratio,
  output logic [15:0] cores_required,
  output logic        must_be_fixed,
  output logic        should_be_programmable,
  output logic [31:0] bce_per_change
);
  always_comb begin
    fixed_bce = 32'(fixed_flops) * 32'(BCE_PER_FLOP);

    cores_required = 16'((32'(program_instructions) * 32'(pps_million))
                         / (32'(CORE_MHZ) * 1000 * 32'(IPC)));

    prog_bce = (32'(cores_required) * CORE_GE * BCE_PER_GE_X100) / 100
             + (32'(cores_required) * IMEM_KB * 1024 * 8);

    ratio = (fixed_bce == 0) ? 16'hFFFF
          : 16'(prog_bce / fixed_bce);

    // Section 4: anything whose cost scales with the payload cannot be
    // a program, whatever the area says.
    must_be_fixed = touches_every_octet;

    // The honest criterion: programmability is worth its area when the
    // function is expected to change.
    should_be_programmable = !touches_every_octet &&
                             (expected_changes_per_year > 16'd0);

    // The figure a product decision actually needs: area per change.
    bce_per_change = (expected_changes_per_year == 0) ? 32'hFFFF_FFFF
                   : ((prog_bce - fixed_bce)
                      / 32'(expected_changes_per_year));
  end
endmodule

Classification: a two-number comparison with one categorical override.

What it teaches: that must_be_fixed overrides everything and is a property of the function, not of the economics. A transformation that touches every octet costs instructions proportional to the payload, and the budget is per packet — so no core count makes it work. Section 4's table: a 1 500-octet AES pass is 750 instructions, more than the entire 64-core budget for one packet. The comparator refuses the question rather than answering it.

And it teaches that bce_per_change is the figure a product decision needs and nobody computes. Programmability costs 2.23 × 10⁷ BCE more than the fixed set; if the function changes twice a year over a five-year life that is 2.23 × 10⁶ BCE per change — and the comparison is then against what a re-spin costs, which is a different currency entirely. The block makes the trade explicit rather than settling it.

Deliberately simplified: cores_required sizes the array from the program's instruction count and one line rate, ignoring that a real array is sized once for all programs it will ever run. prog_bce scales instruction memory with the core count, which is right for a per-core memory and wrong for a shared one. ratio is integer division, so a ratio under 1 reads as 0. And expected_changes_per_year is an input that nobody can supply honestly — it is the whole uncertainty of the decision, reduced to a parameter.

Production implication: the shared-instruction-memory option is the one that changes the arithmetic most and it is under-used. Section 6 showed instruction memory is 59.2 of the array's 78.6 datapaths — three quarters of it — and per-core memories are replicated 64 times to hold the same program. A design with one shared instruction memory and per-core caches holds 32 kB once rather than 64 times: 2.62 × 10⁵ BCE instead of 1.68 × 10⁷, a factor of 64 — at the cost of an instruction-fetch bandwidth problem that is exactly Chapter 23.3 §7's banked-lookup problem in a new place. The trade is real, the saving is enormous, and the reason per-core memories persist is that fetch bandwidth at 64 cores × 1.5 GHz is 96 billion instructions per second.


8. What Actually Moves Off the Host

The commercial case is that a SmartNIC removes work from the host. This section says how much, in the host's own unit.

Section 2's comparison, restated.

Instructions per second
64 cores at 1.5 GHz, 1 IPC96 000 million
one 3 GHz host core at 3 IPC9 000 million
host cores replaced10.7

And per packet, which is how a workload is actually sized.

Instructions per packet at 148.81 Mpps
the 64-core array645.1
one host core60.5
so the array is worth10.7 host cores

Which functions are worth moving, by that measure.

FunctionInstructions per packetHost cores at 148.81 Mpps
a virtual switch's flow lookup and action60–1201.0 to 2.0
VXLAN or GENEVE encapsulation50–1000.8 to 1.7
a stateful firewall80–1501.3 to 2.5
a load balancer's connection table60–1101.0 to 1.8
all four250–4804.1 to 7.9

Row five is the case that justifies the part, and it is worth reading carefully: a SmartNIC running all four functions frees between four and eight host cores per 100 Gb/s port. On a host with 64 cores that is 6 to 12 per cent of the machine, recovered for the workload the machine was bought to run.

And the honest counter-argument, which is arithmetic rather than opinion.

Value
the array's area78.6 MAC receive datapaths
the NIC's increase+43.9%
host cores freed4.1 to 7.9
the same cores, bought as host silicona fraction of a server's CPU cost

Whether that trade is worth making depends on numbers this chapter cannot supply — the relative cost of a NIC's die area against a server's CPU sockets, and how many ports share one host. What it can supply is the two quantities, and the decision is then arithmetic rather than a preference.

One asymmetry that decides it in practice. A host core freed on one machine is worth one core; a SmartNIC design is amortised across every machine that ships with it. So the trade favours the NIC when the fleet is large and the function is stable, and favours the host when the fleet is small or the function changes often — which is the opposite of the intuition that programmability suits change. Programmability suits change in the function; the decision to have a programmable part at all suits stability in the deployment.


9. RTL 4 — The Program Loader

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// program_loader -- the moment the device's behaviour is decided.
//
// Every earlier chapter in this track verified a design whose behaviour
// was fixed by the silicon. This block is where that stops being true,
// and Section 20's refused property is what happens to a proof that was
// written before this block ran.
// ---------------------------------------------------------------------
module program_loader
  import smartnic_pkg::*;
#(
  parameter int SLOTS = 8            // programs resident at once
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        load_req,
  input  logic [31:0] load_program_id,
  input  logic [15:0] load_instr_count,
  input  logic [15:0] load_worst_case,
  input  logic        load_signature_ok,
  input  logic        load_verifier_ok,
  input  verification_level_e load_level,
  input  logic        unload_req,
  input  logic [2:0]  unload_slot,

  output logic        load_accepted,
  output logic [2:0]  load_slot,
  output logic [15:0] total_worst_case,
  output logic        budget_exceeded,
  output logic        unverified_resident,
  output logic [2:0]  slots_used,
  output logic [31:0] c_loads,
  output logic [31:0] c_rejected_loads,
  output logic [31:0] c_loads_since_verification
);
  program_t slot [SLOTS];
  logic [SLOTS-1:0] occupied;

  logic [2:0] free_slot;
  logic       any_free;

  always_comb begin
    any_free = 1'b0; free_slot = 3'd0;
    slots_used = 3'd0;
    total_worst_case = 16'd0;
    unverified_resident = 1'b0;
    for (int i = 0; i < SLOTS; i++) begin
      if (!occupied[i]) begin
        if (!any_free) begin any_free = 1'b1; free_slot = 3'(i); end
      end else begin
        slots_used = slots_used + 3'd1;
        // Section 3's production note: programs COMPOSE, so the budget
        // is over the loaded SET and not over any one member.
        total_worst_case = total_worst_case + slot[i].worst_case_instructions;
        if (!slot[i].verified) unverified_resident = 1'b1;
      end
    end

    budget_exceeded = ((total_worst_case + load_worst_case) >
                       16'(instr_per_packet()));

    load_accepted = load_req && any_free && load_signature_ok
                  && load_verifier_ok && !budget_exceeded;
    load_slot = free_slot;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      occupied <= '0; c_loads <= '0; c_rejected_loads <= '0;
      c_loads_since_verification <= '0;
    end else begin
      if (load_accepted) begin
        slot[load_slot].program_id              <= load_program_id;
        slot[load_slot].instruction_count       <= load_instr_count;
        slot[load_slot].worst_case_instructions <= load_worst_case;
        slot[load_slot].bounded_loops           <= (load_level >= VER_BUDGET);
        slot[load_slot].verified                <= (load_level == VER_FULL);
        occupied[load_slot] <= 1'b1;
        c_loads <= c_loads + 32'd1;
        // The counter that names the chapter's problem: how many
        // behaviours this device has had since anybody proved anything
        // about it.
        c_loads_since_verification <= c_loads_since_verification + 32'd1;
      end else if (load_req) begin
        c_rejected_loads <= c_rejected_loads + 32'd1;
      end
      if (unload_req) occupied[unload_slot] <= 1'b0;
    end
  end
endmodule

Classification: an admission control for behaviour, and its most telling output is a count of how many behaviours the device has had.

What it teaches: that budget_exceeded is computed over the loaded set and not over the arriving program. Two programs that each fit in 645 instructions do not both fit — 400 plus 400 is 800 — so the loader must hold a running sum and refuse a load that breaks it. That is a global check performed at line rate, on a device whose whole point is that programs arrive while it runs.

And it teaches that c_loads_since_verification is the number a compliance argument needs and no device reports. Every load changes the device's behaviour; a proof written before the first load ranged over the programs that existed then. A device that has been loaded forty times since anything was proved about it is running a behaviour nobody has examined, and that fact is a counter rather than a philosophy.

Deliberately simplified: budget_exceeded sums worst cases, which is correct and conservative, and a real device would want the sum of worst cases along a single packet's path rather than across all resident programs — a packet usually runs one. load_verifier_ok is an input: the verifier itself is Section 11. There is no notion of a program being active rather than merely resident, and eight resident programs of which one runs is a very different budget from eight that all run. And an unload does not check whether a packet is mid-program in that slot.

Production implication: the mid-program unload is the hazard that turns a management operation into a corruption, and it is the same shape as Chapter 23.3 §7's table update. A program unloaded while a packet is executing it leaves that packet running instructions from a slot that is being overwritten — the packet's behaviour is then a mixture of two programs, which is neither and was never verified as either. The remedy is the same one that chapter found: a valid bit, cleared first, so no new packet enters the slot, and a drain before the memory is reused. The drain is bounded by the worst-case program length — 1 200 instructions is 800 nanoseconds at 1.5 GHz — so it costs under a microsecond and is the difference between an atomic operation and an undefined one.


10. The Device's Behaviour Is Decided After It Ships

Every chapter in modules nineteen to twenty three has verified a design whose behaviour was fully determined by its silicon: a media access control block, a physical layer device, a switch pipeline, a fixed function network interface card. A programmable card is the first whose behaviour is a function of the silicon and a program, and the program arrives after the silicon does. So a property of the form for every program the invariant holds was proved over the programs that existed when it was written, and the device's future programs are not in that set. The property was correctly scoped when written and becomes wrongly scoped the moment a new program is loaded. Three things this does not mean. It does not mean nothing can be proved, because the silicon's own properties still hold regardless of the program. It does not mean programs are unverifiable, because termination, an instruction budget and memory safety are all decidable for the restricted languages real programmable cards accept, since those languages forbid unbounded loops and the general halting problem is therefore not the question being asked. And it does not mean the part is unsafe; it means the proof's domain must be enforced rather than assumed. The enforcement is a verifier in the load path that checks a signature, the absence of unbounded loops, a longest path within the instruction budget, and memory accesses confined to the program's own region. That mechanism is a refusal rather than a measurement, which makes it the first prevention mechanism in the track: the nine previous refused property classes were all closed by observing something, and this one is closed by declining to load.A fixed-functionpartbehaviour fixed attape-outA programmablepartsilicon plus a programThe proof'sdomainthe programs thatexistedThe deployeddomainopen — every validsequenceIt becomes falsesilentlyno event, no counterA verifier in theload pathsignature, halting,budget, memoryA refusal, not ameasurementthe track's firstprevention12
Figure 3 — the first part in the track whose behaviour is not determined by the thing that was verified.

Every chapter in Modules 19 to 23 has verified a design. This is the first whose behaviour is not fully determined by the thing that was verified.

The difference, stated precisely.

A fixed-function partA programmable part
behavioura function of the silicona function of the silicon and a program
when it is fixedat tape-outat each load
what verification examinedthe behaviourthe silicon, and the programs that existed
the domain of a proofclosedopen, unless something closes it
who can change itnobodywhoever can load a program

Row four is the chapter's verification problem in one line. A property of the form for every program P, invariant I holds is proved over the programs that existed when it was written, and the device's future programs are not in that set. The property was correctly scoped when written and becomes wrongly scoped the moment a new program is loaded.

And the scale of the gap is a counter, not a philosophy.

Value
programs verified before tape-outhowever many the team wrote
programs the device can runevery valid instruction sequence
programs actually loaded over a five-year lifec_loads_since_verification
what enforces the proof's domain at load timenothing, by default

Three things this does not mean, because the argument is easy to overstate.

Why not
it does not mean nothing can be provedthe silicon's properties still hold — Section 16's ten
it does not mean programs are unverifiableSection 12 — termination, budget and memory safety are decidable here
it does not mean the part is unsafeit means the domain must be enforced rather than assumed

Row two is the important one and it rests on a restriction. A general-purpose program's termination is undecidable; a packet-processing program's is not, because the languages real SmartNICs accept forbid unbounded loops. Restricting the language converts an undecidable question into a decidable one, and that restriction is the load-time verifier's whole foundation.

The fix for an open quantification is not a stronger property. It is a mechanism in the load path that makes the quantification closed again.

Which is a different kind of answer from any earlier chapter's.

ChapterThe gapThe fix
Chapter 21.9 §20an instrument's completenessan independent witness — a port counter
Chapter 22.2 §20a relation between nodesimport a number
Chapter 23.1 §20capacity against reachabilitymeasure the second
this chapter, Section 20an open program seta gate in the load path

Rows one to three add a measurement. Row four adds a refusal — the verifier does not observe the program, it declines to load one that falls outside the domain — and that is the first mechanism in the track that works by prevention rather than by detection.


11. RTL 5 — The Program Verifier

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// program_verifier -- the gate that closes Section 10's open
// quantification.
//
// It does not prove the program correct. It proves the program is
// INSIDE THE DOMAIN the device's properties were written over: bounded
// loops, a bounded instruction count, and memory accesses confined to
// the program's own region.
// ---------------------------------------------------------------------
module program_verifier
  import smartnic_pkg::*;
#(
  parameter int MAX_INSTR   = 645,
  parameter int MAX_LOOPS   = 0        // unbounded loops forbidden
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        verify_req,
  input  logic [15:0] instr_count,
  input  logic [15:0] backward_branches,
  input  logic [15:0] unbounded_loops,
  input  logic [15:0] max_memory_offset,
  input  logic [15:0] program_region_size,
  input  logic        signature_valid,
  input  logic [15:0] worst_case_path,

  output logic        verdict_ok,
  output verification_level_e level,
  output logic        fails_termination,
  output logic        fails_budget,
  output logic        fails_memory,
  output logic        fails_signature,
  output logic [31:0] c_verified,
  output logic [31:0] c_rejected,
  output logic        domain_is_closed
);
  always_comb begin
    // 1. Termination: no unbounded loops. Decidable because the
    //    language forbids them, not because the general problem is.
    fails_termination = (unbounded_loops > 16'(MAX_LOOPS));

    // 2. Budget: the worst-case path fits the per-packet allowance.
    fails_budget = (worst_case_path > 16'(MAX_INSTR));

    // 3. Memory safety: every access is inside the program's region.
    fails_memory = (max_memory_offset >= program_region_size);

    // 4. Provenance.
    fails_signature = !signature_valid;

    verdict_ok = verify_req && !fails_termination && !fails_budget
               && !fails_memory && !fails_signature;

    if (!verify_req)           level = VER_NONE;
    else if (fails_signature)  level = VER_NONE;
    else if (fails_termination)level = VER_SIGNATURE;
    else if (fails_budget)     level = VER_TERMINATION;
    else if (fails_memory)     level = VER_BUDGET;
    else                       level = VER_FULL;

    // The claim the whole mechanism exists to support.
    domain_is_closed = (level == VER_FULL);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_verified <= '0; c_rejected <= '0;
    end else if (verify_req) begin
      if (verdict_ok) c_verified <= c_verified + 32'd1;
      else            c_rejected <= c_rejected + 32'd1;
    end
  end
endmodule

Classification: four checks, of which three are decidable only because the language was restricted.

What it teaches: that fails_termination is answerable at all only because MAX_LOOPS is zero. The halting problem is undecidable for a general language and trivially decidable for one with no backward branches — or with only counted loops whose bounds are literals. The verifier does not solve a hard problem; the language designer removed it, and that removal is the reason a SmartNIC's programming model looks restrictive.

And it teaches that level is a ladder whose rungs fail in a useful order. A program that fails the signature never gets examined; one that passes provenance and fails termination is a bug in a trusted program; one that terminates and exceeds the budget is a performance statement rather than a safety one. The ladder tells the operator which kind of problem they have, which a single Boolean does not.

Deliberately simplified: worst_case_path is an input the verifier consumes rather than computes — the actual analysis is a longest-path walk over the program's control-flow graph, which is where the work is. fails_memory compares one maximum offset against one region size, modelling a program with a single flat region; real programs touch packet data, per-flow state and shared tables, each with its own bounds. MAX_INSTR is a constant where the real budget is the loaded set's, from Section 9. And nothing here verifies interaction between programs, which Section 9's production note showed is the case that matters.

Production implication: the offline-versus-online split is the practical decision, and getting it wrong makes the verifier either useless or unaffordable. A longest-path analysis over a control-flow graph is polynomial and cheap on a host and expensive in a device that must also forward at 148.81 Mpps. The workable arrangement is to do the analysis offline, emit the verdict and the worst-case path length as signed metadata, and have the device check only the signature and the arithmetic — which is what program_verifier above actually does. That reduces the device's job to a signature check and three comparisons, and it moves the trust to whoever signs. The alternative — a full in-device verifier — has been built, and it costs more area than the array it protects.


12. What a Verifier Can and Cannot Establish

Section 11's four checks close the domain. This section says exactly which claims they support and which they do not, because the distinction is where trust is usually misplaced.

What the verifier establishes.

ClaimEstablished byDecidable because
the program came from a trusted partythe signaturecryptography
the program haltsno unbounded loopsthe language forbids them
it halts within N instructionsa longest-path walkthe control-flow graph is acyclic
it touches only its own memoryoffset boundsthe addressing mode is restricted

What it does not establish, and cannot.

ClaimWhy not
the program is correctcorrectness is relative to an intent the device never sees
the program does what its name saysthe same
the composition of two programs is correctSection 9 — interaction is not a per-program property
the program will not degrade throughputit will, if its worst case is large — and that is legal
the program is not malicious within its boundsa program can be memory-safe, terminating, in budget and hostile

Row five deserves stating plainly because the four checks are often described as making a device safe. A program that drops every packet from one tenant, or mirrors traffic to an address of its choosing, or rewrites a header to bypass a policy is terminating, in budget and memory-safe. The verifier admits it, correctly, because those are behaviours rather than violations — and distinguishing them requires knowing what the device is supposed to do, which the verifier does not.

The verifier establishes that the program is inside the domain the device's properties were written over. It establishes nothing about whether the program should be running.

Which makes the trust model explicit and is worth writing down.

Who is trustedFor what
the signaturewhoever holds the keythat the program is authorised
the verifierthe devicethat the program is in the domain
the program's behaviournobodyit is not checked at all

Row three is the correct arrangement and it is frequently misunderstood. The device is not deciding whether the program is a good idea; it is enforcing that the program cannot break the device. The question of whether the program should run is answered by the signature, which means the whole trust argument reduces to key management — a completely different discipline from anything in this track, and the one that actually secures the arrangement.

And the load-time checks have a cost that is worth naming.

CheckWhere it runsCost
longest-path analysisoffline, on a hostpolynomial in the program size
memory-bound analysisofflinethe same
signature verificationin the device, at loadone public-key operation
the three arithmetic comparisonsin the devicea few dozen flops

The device's share is a signature check and three comparisons, which is why Section 11's block is small. Everything expensive happens offline and arrives as signed metadata, and the device's job is to refuse anything whose metadata does not check out.


13. RTL 6 — Per-Packet Determinism, and Why It Is Gone

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// determinism_monitor -- a fixed-function pipeline has constant
// latency. A programmable one has latency that depends on the packet.
//
// Chapter 23.3 Section 17 established that a switch's pipeline is
// fixed-latency and its buffer is not. A SmartNIC's PIPELINE is not
// fixed-latency either, and the variability is a function of packet
// CONTENT rather than of load.
// ---------------------------------------------------------------------
module determinism_monitor
  import smartnic_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        pkt_done,
  input  logic [15:0] instructions_taken,
  input  logic [15:0] budget,
  input  logic [15:0] pps_million,

  output logic [15:0] min_instructions,
  output logic [15:0] max_instructions,
  output logic [31:0] mean_instructions_x100,
  output logic [15:0] spread_ratio_x10,
  output logic [15:0] sustained_at_max_mpps,
  output logic [15:0] sustained_at_max_pct,
  output logic        content_dependent,
  output logic        adversary_can_halve,
  output logic [31:0] c_packets,
  output logic [31:0] c_over_budget
);
  logic [31:0] total_instructions;

  always_comb begin
    spread_ratio_x10 = (min_instructions == 0) ? 16'hFFFF
                     : (max_instructions * 16'd10) / min_instructions;

    // The throughput the array sustains on the WORST path, which is the
    // only figure a deadline or an adversary cares about.
    sustained_at_max_mpps = (max_instructions == 0) ? 16'hFFFF
                          : 16'((32'(CORES) * CORE_MHZ * 32'(IPC))
                                / 32'(max_instructions));
    sustained_at_max_pct = (pps_million == 0) ? 16'd0
                         : (sustained_at_max_mpps * 16'd100) / pps_million;

    // Always true for a programmable datapath, and it is the point.
    content_dependent = 1'b1;

    // If the worst path is more than twice the budget, a sender who can
    // steer packets onto it halves the part's throughput.
    adversary_can_halve = (max_instructions > (budget * 16'd2));
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      min_instructions <= 16'hFFFF; max_instructions <= '0;
      total_instructions <= '0; c_packets <= '0; c_over_budget <= '0;
      mean_instructions_x100 <= '0;
    end else if (pkt_done) begin
      if (instructions_taken < min_instructions)
        min_instructions <= instructions_taken;
      if (instructions_taken > max_instructions)
        max_instructions <= instructions_taken;
      total_instructions <= total_instructions + 32'(instructions_taken);
      c_packets <= c_packets + 32'd1;
      if (instructions_taken > budget)
        c_over_budget <= c_over_budget + 32'd1;
      if (c_packets != 0)
        mean_instructions_x100 <= (total_instructions * 100)
                                  / (c_packets + 32'd1);
    end
  end
endmodule

Classification: a distribution monitor whose most important output is the maximum, not the mean.

What it teaches: that sustained_at_max_pct is the only throughput figure a programmable datapath can honestly quote. A program with a mean of 400 instructions and a worst case of 1 200 sustains 240 Mpps at the mean and 80 at the worst — 53.8% of line rate — and the traffic that triggers the worst path is decided by packet content. Every benchmark measures the mean; every deadline and every adversary cares about the maximum.

And it teaches that adversary_can_halve is a security property expressed as arithmetic. If the worst path is more than twice the budget, a sender who can construct packets that take it halves the part's throughput — without malformed frames, without a protocol violation, and without anything a firewall would notice. That is a denial of service performed entirely with valid traffic, and its magnitude is budget ÷ worst_case.

Deliberately simplified: min_instructions and max_instructions are running extrema with no decay, so a single outlier fixes the maximum for the device's life — a real monitor needs a windowed or percentile-based measure. mean_instructions_x100 divides by c_packets + 1 to avoid a divide-by-zero, which biases it. There is no per-program breakdown, so a device running eight programs reports one distribution. And content_dependent is a literal, which is honest: it is a claim about the architecture rather than a measurement.

Production implication: the missing per-program breakdown is what makes the adversary case hard to attribute, and separating it is cheap. A device running eight programs and showing a 1 200-instruction maximum does not say which program produced it, so an operator cannot tell whether the long path is a rarely-taken branch in a well-behaved program or a hostile input to a badly-written one. Per-program extrema are eight sets of two 16-bit registers — 256 flops, 5 120 BCE, 0.02 of a MAC receive datapath — and they turn the device occasionally takes 1 200 instructions into program 5 has a 1 200-instruction path and it is being taken 0.3% of the time. The second is actionable and the first is not.


14. What a SmartNIC Must Never Do

Five prohibitions, each derived earlier.

One — never size the array from a program's mean instruction count.

SustainedOf line rate
mean 400 instructions240 Mpps161%
worst case 1 20080 Mpps53.8%

The traffic that triggers the worst path is chosen by packet content, so the mean describes a benchmark and the maximum describes a deployment.

Two — never put a payload transformation in a program.

Section 4: the budget is per packet and a payload cost is per octet. AES-GCM over 1 500 octets at two octets per instruction is 750 instructions — more than the entire 64-core budget for one packet. Anything that touches every byte must be fixed function, whatever the area comparison says.

Three — never load a program without closing the domain.

Section 10: a proof written before a load ranges over the programs that existed then. The gate is Section 11's verifier — signature, termination, budget, memory bounds — and the device's share of it is one public-key operation and three comparisons. Everything expensive runs offline and arrives as signed metadata.

Four — never treat a verifier's pass as a statement about behaviour.

The verifier establishesIt does not establish
the program haltsthat it is correct
it fits the budgetthat it does what its name says
it touches only its own memorythat it is not hostile within those bounds
it came from a trusted keythat the key-holder was right to sign it

A program that drops one tenant's traffic is terminating, in budget and memory-safe. The whole behavioural trust reduces to key management, which is a different discipline and the one that actually secures the arrangement.

Five — never unload a program while a packet is executing it.

Section 9's production note. A packet mid-program in a slot being overwritten runs a mixture of two programs — neither of which was verified. The remedy is Chapter 23.3 §7's: clear a valid bit first so no new packet enters, then drain. The drain is bounded by the worst-case path — 1 200 instructions is 800 ns at 1.5 GHz — so it costs under a microsecond.


15. RTL 7 — SmartNIC Telemetry

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// smartnic_telemetry -- what a programmable part knows about itself,
// plus the one field no fixed-function part needs: how many times its
// behaviour has changed.
// ---------------------------------------------------------------------
module smartnic_telemetry
  import smartnic_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,
  input  logic        snapshot,

  input  logic [31:0] i_packets,
  input  logic [31:0] i_rejected_by_array,
  input  logic [15:0] i_min_instructions,
  input  logic [15:0] i_max_instructions,
  input  logic [31:0] i_mean_instructions_x100,
  input  logic [31:0] i_over_budget,
  input  logic [15:0] i_utilisation_pct,
  input  logic [31:0] i_loads,
  input  logic [31:0] i_rejected_loads,
  input  logic [31:0] i_loads_since_verification,
  input  logic [2:0]  i_slots_used,
  input  logic        i_unverified_resident,
  input  logic [15:0] i_total_worst_case,

  output logic [31:0] o_packets,
  output logic [15:0] o_min_instructions,
  output logic [15:0] o_max_instructions,
  output logic [31:0] o_mean_instructions_x100,
  output logic [15:0] o_spread_ratio_x10,
  output logic [15:0] o_over_budget_ppm,
  output logic [15:0] o_rejected_ppm,
  output logic [15:0] o_utilisation_pct,
  output logic [31:0] o_loads_since_verification,
  output logic [15:0] o_budget_used_pct,
  output logic [2:0]  o_caveats,
  output logic        o_behaviour_is_verified
);
  localparam int C_UNVERIFIED  = 0;
  localparam int C_OVER_BUDGET = 1;
  localparam int C_SPREAD      = 2;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      o_packets <= '0; o_min_instructions <= 16'hFFFF;
      o_max_instructions <= '0; o_mean_instructions_x100 <= '0;
      o_spread_ratio_x10 <= 16'd10; o_over_budget_ppm <= '0;
      o_rejected_ppm <= '0; o_utilisation_pct <= '0;
      o_loads_since_verification <= '0; o_budget_used_pct <= '0;
      o_caveats <= '0;
    end else if (snapshot) begin
      o_packets                  <= i_packets;
      o_min_instructions         <= i_min_instructions;
      o_max_instructions         <= i_max_instructions;
      o_mean_instructions_x100   <= i_mean_instructions_x100;
      o_utilisation_pct          <= i_utilisation_pct;
      o_loads_since_verification <= i_loads_since_verification;

      if (i_min_instructions != 0)
        o_spread_ratio_x10 <= (i_max_instructions * 16'd10)
                              / i_min_instructions;
      if (i_packets != 0) begin
        o_over_budget_ppm <= 16'((i_over_budget * 1_000_000) / i_packets);
        o_rejected_ppm    <= 16'((i_rejected_by_array * 1_000_000)
                                 / i_packets);
      end

      o_budget_used_pct <= 16'((32'(i_total_worst_case) * 100)
                               / 32'(instr_per_packet()));

      o_caveats[C_UNVERIFIED]  <= i_unverified_resident;
      o_caveats[C_OVER_BUDGET] <= (i_over_budget != 32'd0);
      // A spread above 3x means the worst case is far from the mean,
      // which is Section 13's adversary case.
      o_caveats[C_SPREAD]      <= (o_spread_ratio_x10 > 16'd30);
    end
  end

  // The claim a compliance argument needs, and no fixed-function part
  // ever had to make.
  assign o_behaviour_is_verified = !o_caveats[C_UNVERIFIED];
endmodule

Classification: a snapshot bank with one field that exists only because the part is programmable.

What it teaches: that o_loads_since_verification has no analogue in any earlier chapter. A MAC, a PHY, a switch or a fixed-function NIC has one behaviour, verified once. A SmartNIC has had as many behaviours as it has had loads, and a device that reports forty since anything was proved about it is making a statement about the standing of its own verification. It is a counter, it costs 32 flops, and it is the only honest summary of the situation.

And it teaches that o_spread_ratio_x10 is the field that surfaces Section 13's adversary case. A spread of 3× or more between the minimum and maximum instruction counts means the worst path is far from the typical one, so a sender who can steer packets onto it has a lever. The ratio costs one division and turns an architectural property into an observation.

Deliberately simplified: no read-clear, and at 148.81 Mpps a 32-bit packet counter wraps in 28.9 secondsChapter 23.4 §15's problem, unchanged. o_min_instructions and o_max_instructions are lifetime extrema with the decay problem Section 13 named. o_budget_used_pct sums worst cases across all resident programs, which over-counts when only one runs per packet. And there is no per-program breakdown, which Section 13's production note argued for.

Production implication: o_loads_since_verification is the field to expose to a compliance process rather than to an operator, and the distinction matters. An operator wants to know whether the device is working; a compliance argument wants to know whether the thing that was certified is the thing that is running. For a part whose behaviour is a program, those are different questions, and the second is answered by a program-set hash rather than a load count — a digest over the identifiers of every resident program, which changes on every load and unload. Thirty-two bits, updated on two events, and it lets an auditor ask "is this the configuration that was approved" and get an answer.


16. RTL 8 — The SmartNIC Conformance Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// smartnic_conformance_monitor -- properties for a programmable NIC.
//
// Every property here is about the SILICON, because the silicon is the
// only part whose behaviour was fixed at tape-out. Properties 11 and 12
// are the two that constrain what the silicon does with a program, and
// they are the whole of what can be asserted about behaviour.
// ---------------------------------------------------------------------
module smartnic_conformance_monitor
  import smartnic_pkg::*;
(
  input logic        clk,
  input logic        rst_n,

  input logic [31:0] budget_per_packet,
  input logic [15:0] cores_busy,
  input logic        accepted,
  input logic        array_saturated,
  input logic        pkt_arrives,
  input logic [15:0] instructions_taken,
  input logic [15:0] program_worst_case,
  input logic        load_req,
  input logic        load_accepted,
  input logic        load_signature_ok,
  input logic        load_verifier_ok,
  input logic        budget_exceeded,
  input logic        verdict_ok,
  input logic        fails_termination,
  input logic        fails_budget,
  input logic        fails_memory,
  input logic        domain_is_closed,
  input logic        unverified_resident,
  input logic        behaviour_is_verified,
  input logic        claim_conformance
);
  // 1. A packet is accepted only if a core is free.
  p_sn_accept: assert property (@(posedge clk) disable iff (!rst_n)
    accepted |-> (cores_busy < 16'(CORES)));

  // 2. Saturation and acceptance are exclusive.
  p_sn_exclusive: assert property (@(posedge clk) disable iff (!rst_n)
    !(accepted && array_saturated));

  // 3. Every arrival is either accepted or counted as rejected.
  p_sn_accounted: assert property (@(posedge clk) disable iff (!rst_n)
    pkt_arrives |-> (accepted || array_saturated));

  // 4. A packet never executes more than its program's worst case.
  p_sn_worst_case: assert property (@(posedge clk) disable iff (!rst_n)
    instructions_taken <= program_worst_case);

  // 5. A load requires a valid signature.
  p_sn_load_sig: assert property (@(posedge clk) disable iff (!rst_n)
    load_accepted |-> load_signature_ok);

  // 6. And a verifier pass.
  p_sn_load_ver: assert property (@(posedge clk) disable iff (!rst_n)
    load_accepted |-> load_verifier_ok);

  // 7. And budget headroom over the loaded SET.
  p_sn_load_budget: assert property (@(posedge clk) disable iff (!rst_n)
    load_accepted |-> !budget_exceeded);

  // 8. The verifier's verdict is the conjunction of its four checks.
  p_sn_verdict: assert property (@(posedge clk) disable iff (!rst_n)
    verdict_ok == (!fails_termination && !fails_budget && !fails_memory));

  // 9. A closed domain requires a full verification level.
  p_sn_domain: assert property (@(posedge clk) disable iff (!rst_n)
    domain_is_closed |-> verdict_ok);

  // 10. An unverified resident program clears the verified flag.
  p_sn_unverified: assert property (@(posedge clk) disable iff (!rst_n)
    unverified_resident |-> !behaviour_is_verified);

  // 11. A CONFORMANCE CLAIM requires every resident program to be
  //     inside the domain. This is the property Section 20 is about.
  p_sn_claim_domain: assert property (@(posedge clk) disable iff (!rst_n)
    claim_conformance |-> behaviour_is_verified);

  // 12. And the silicon's own properties to hold, which they do
  //     regardless of the program.
  p_sn_silicon_holds: assert property (@(posedge clk) disable iff (!rst_n)
    claim_conformance |-> (accepted || array_saturated || !pkt_arrives));
endmodule

Classification: twelve properties about silicon, and two of them are the only statements about behaviour that exist.

What it teaches: that property 4 is the one that depends on something outside the silicon. instructions_taken <= program_worst_case is checkable at run time only because the worst case arrived as signed metadata — the silicon does not compute it. Every other property here is a statement about gates, and this one imports a number from a verifier that ran offline.

And it teaches that property 11 is the whole behavioural guarantee. It says a conformance claim requires every resident program to be inside the domain — not that they are correct, not that they are safe, but that they are the kind of program the device's other eleven properties were written over. That is a weaker claim than any earlier chapter's conformance monitor makes, and it is the strongest one available.

Deliberately simplified: property 4 compares against the program's worst case rather than the array's budget, so a program whose metadata lies is not caught. Property 8 omits the signature from the conjunction, matching Section 11's ladder where a signature failure short-circuits. Property 12 is nearly vacuous — it restates property 3 — and is kept to make explicit that the silicon's properties are unconditional. And no property here says anything about what a program computes, which is Section 12's point stated as an absence.

Production implication: property 4's dependence on signed metadata is the seam where the whole arrangement can be attacked, and it is worth hardening. If a program's declared worst case is smaller than its real one, the loader admits it, the budget arithmetic is wrong and the array saturates — and property 4 fires, which is the correct behaviour and happens after the damage. A run-time instruction counter per packet with a hard cap — kill the packet at the budget, count it, and raise a caveat — turns a lie in the metadata into a bounded, counted event rather than a throughput collapse. It costs a comparator and a counter per core: 64 × 32 bits = 2 048 flops, 40 960 BCE, 0.14 of a MAC receive datapath.


17. The Three Architectures, Compared

The same packet processing work can live in three places and the comparison is now in one unit. Fixed function logic gives effectively unbounded parallel computation, costs zero point four zero media access control receive datapaths of area, consumes no host cores, has fixed latency, has its behaviour decided at tape out, permits everything to be proved about it, cannot be changed after shipping, and its worst case under hostile input is unchanged from its typical case. A programmable array gives six hundred and forty five instructions per packet at sixty four cores, costs seventy eight point six datapaths, consumes no host cores, has content dependent latency, has its behaviour decided at each program load, permits its domain to be proved if a verifier enforces it, can be changed after shipping, and its throughput is halved under hostile input that steers packets onto a long path. Host software gives sixty point five instructions per packet per core, costs no area on the card but four point one to seven point nine host cores for a realistic function set, has content dependent latency plus scheduling, has its behaviour decided at deployment, permits the card to prove nothing about it, can be changed at any time, and is worse still under hostile input. The decision rule that follows is that a function touching every octet must be fixed, a function that is stable and ships in volume should be fixed because a programmable array costs one hundred and ninety seven times the blocks it replaces, a function that changes more than once a year should be programmable, a per deployment or per customer function belongs in host software or in a signed program, and a function on a latency critical path facing hostile input should be fixed.The same workthree placesFixed function0.40 datapaths, fixedlatencyProgrammablearray78.6 datapaths, 645instrHost software0 area, 4.1–7.9 coresTouches everyoctet?→ fixed, no core countworksStable and involume?→ fixed, 197× says soChanges yearly?→ programmableHostile input ona deadline?→ fixed — throughputhalves12
Figure 4 — the same work, three places, and the decision rule that falls out.

Fixed function, programmable datapath, host software — the same work, three places, and the comparison is now in one unit.

Fixed functionProgrammable arrayHost software
instructions per packetunbounded, in parallel645 at 64 cores60.5 per core
area on the NIC0.40 datapaths78.60
host cores consumed004.1 to 7.9
latencyfixedcontent-dependentcontent-dependent, plus scheduling
behaviour decidedat tape-outat program loadat deployment
what can be provedeverythingthe domain, if a verifier enforces itnothing, by the NIC
changeable after shippingnoyesyes
worst case under hostile inputunchangedSection 13 — halvedworse

Row eight is the one that is usually left out. A fixed-function block's throughput does not depend on packet content; a programmable array's does, and a sender who can steer packets onto the long path halves it. Host software is worse still, because it adds scheduling and cache behaviour to the same effect.

And the decision rule that falls out.

If the functionThen
touches every octetfixed function — Section 4, no core count works
is stable and ships in volumefixed function — 0.40 datapaths against 78.6
changes more than once a yearprogrammable — Section 7's bce_per_change
is per-deployment or per-customerhost software, or programmable with signing
is on a latency-critical path with a hostile inputfixed function — row eight

Row two is the one that decides most cases and it is the one an enthusiasm for programmability tends to skip. A programmable array costs 197 times the fixed-function blocks it replaces; for a function that is not going to change, that is 197× of area purchased for an option nobody will exercise.

The three architectures' place in the track.

ChapterWhat it built
fixed functionChapter 18.7checksum, segmentation, RSS, queues — 5 650 flops
the NIC around itChapter 23.4descriptors, context cache, host budget — 179 datapaths
the programmable arraythis chapter64 cores, a loader, a verifier — 78.6 datapaths
the hostChapter 18.1the software the NIC exists to relieve

A SmartNIC is Chapter 23.4's NIC, 44% larger, with the option to change its mind. Everything in this chapter is the price and the conditions of that option.


18. What the SmartNIC Assumes

Seven premises, and three of them are about a program that does not exist yet.

AssumptionIf it is false
the program's worst-case path fits the budgetSection 13 — 53.8% of line rate at a 1 200-instruction path
the declared worst case is the real oneSection 16 — the budget arithmetic is wrong and the array saturates
loaded programs compose within the budgetSection 9 — 400 plus 400 is 800 against 645
the language forbids unbounded loopsSection 11 — termination stops being decidable
the signing key is held by somebody trustworthySection 12 — the entire behavioural trust is that key
packets are balanced across coresSection 5 — run-to-completion is balls in bins
memory accesses do not stallSection 5 — three missing lookups are 600 of a 645 budget

Rows one to three are all about a program, and none of them can be checked by the silicon alone. Row one and row three are checked by signed metadata; row two is checked by a run-time counter with a hard cap, which Section 16's production note argued for at 0.14 datapaths.

Row five is the assumption that carries the most weight and receives the least attention in a hardware discussion. Section 12: the verifier establishes that a program is in the domain and nothing about whether it should run. A program that is terminating, in budget and memory-safe can still drop one tenant's traffic or mirror it somewhereso the whole behavioural guarantee is that somebody trustworthy signed it, and the security of the arrangement is key management rather than anything in this chapter.

And one premise that is stated in every product brief and is false in an interesting way.

That programmability makes the part future-proof. It makes the part's function changeable; it does not change the instruction budget, which is fixed at tape-out by the core count and the clock. A part with 645 instructions per packet can be given a different program and cannot be given a bigger budget — so a future requirement that needs 1 500 instructions per packet is as unreachable on a SmartNIC as on a fixed-function part, and the flexibility that was bought does not extend in the direction the requirement grew.


19. The Cost, Accounted — in BCE

Chapter 23.3 §2's unit, applied to this chapter's blocks and to the array they manage.

BlockFlopsBCE× a MAC datapath
instruction_budget0 — combinational0
core_array control1 10022 0000.08
the 64 cores it schedules5.49 × 10⁶19.4
their instruction memories1.68 × 10⁷59.2
offload_comparator0 — combinational0
program_loader4208 4000.03
program_verifier641 2800.005
determinism_monitor1603 2000.011
smartnic_telemetry3406 8000.024
smartnic_conformance_monitor0 — assertions0
all control logic2 08441 6800.15
the array2.23 × 10⁷78.6

The control logic that makes a programmable array safe is 0.15 of a MAC receive datapath — the loader, the verifier, the determinism monitor and the telemetry together — against an array of 78.6. The ratio is 535 : 1, and it is the same shape as every instrumentation argument in Modules 21 to 23: the mechanism that makes a structure trustworthy is a rounding error against the structure.

And the additions Sections 13 and 16 argued for.

AdditionBCE× a MAC datapath× the array
per-program instruction extrema, 8 programs5 1200.0180.023%
a per-core run-time budget cap40 9600.1450.18%
a program-set hash6400.0020.003%
all three46 7200.1650.21%

Two tenths of one per cent of the array, and between them they supply which program takes the long path, a hard cap when the metadata lied, and a digest an auditor can compare against an approved configuration.

The chapter's totals, set against the rest of Module 23.

BCE× a MAC datapath
Chapter 18.7's fixed-function offload set1.13 × 10⁵0.40
Chapter 23.3's switch pipeline logic2.37 × 10⁵0.84
this chapter's programmable array2.23 × 10⁷78.6
Chapter 23.4's fixed-function NIC5.07 × 10⁷179.0
the SmartNIC — NIC plus array7.30 × 10⁷257.6
Chapter 23.3's whole switch5.62 × 10⁸1 985

Row three against row two is the number to keep. A 64-core programmable array is ninety-four times the forwarding logic of a 6.4 Tb/s switchnot because the array is enormous, but because a switch's pipeline is remarkably small, which Chapter 23.3 §19 established and this table confirms from the other side.


20. Properties Worth Asserting, and One Worth Refusing

Six groups. Every property here is about the silicon, because the silicon is the only thing whose behaviour was fixed when the properties were written.

Group A — the budget.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A1. The budget is the array's throughput over the packet rate.
p_bd_divide: assert property (@(posedge clk) disable iff (!rst_n)
  (pps_million != 0) |->
    (budget_per_packet == ((cores * core_mhz * ipc) / pps_million)));

// A2. Sustained throughput at the worst case is the array over it.
p_bd_worst: assert property (@(posedge clk) disable iff (!rst_n)
  (program_worst_instr != 0) |->
    (sustained_mpps_worst ==
      ((cores * core_mhz * ipc) / program_worst_instr)));

// A3. Fitting at the worst case implies fitting at the mean.
p_bd_implies: assert property (@(posedge clk) disable iff (!rst_n)
  fits_at_worst |-> fits_at_mean);

// A4. The host-core equivalence is array MIPS over host MIPS.
p_bd_host: assert property (@(posedge clk) disable iff (!rst_n)
  host_cores_equivalent ==
    ((cores * core_mhz * ipc) / (HOST_MHZ * HOST_IPC)));

// A5. Cores needed scales linearly with the worst case.
p_bd_linear: assert property (@(posedge clk) disable iff (!rst_n)
  cores_needed_worst ==
    ((program_worst_instr * pps_million) / (core_mhz * ipc)));

Group B — the array.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// B1. Acceptance requires a free core.
p_ar_free: assert property (@(posedge clk) disable iff (!rst_n)
  accepted |-> (cores_busy < 16'(CORES)));

// B2. Acceptance and saturation are exclusive.
p_ar_exclusive: assert property (@(posedge clk) disable iff (!rst_n)
  !(accepted && array_saturated));

// B3. Every arrival is accounted.
p_ar_accounted: assert property (@(posedge clk) disable iff (!rst_n)
  pkt_arrives |-> (accepted || array_saturated));

// B4. A run-to-completion assignment holds the core for the program.
p_ar_holds: assert property (@(posedge clk) disable iff (!rst_n)
  (accepted && run_to_completion) |=>
    (core_remaining[$past(assigned_core)] == $past(pkt_instructions) - 1));

// B5. Utilisation is busy cores over total.
p_ar_util: assert property (@(posedge clk) disable iff (!rst_n)
  utilisation_pct == ((cores_busy * 100) / CORES));

// B6. Core occupancy never exceeds the longest program.
p_ar_occupancy: assert property (@(posedge clk) disable iff (!rst_n)
  longest_occupancy <= program_worst_case);

Group C — the loader.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// C1. A load requires a signature.
p_ld_sig: assert property (@(posedge clk) disable iff (!rst_n)
  load_accepted |-> load_signature_ok);

// C2. And a verifier pass.
p_ld_ver: assert property (@(posedge clk) disable iff (!rst_n)
  load_accepted |-> load_verifier_ok);

// C3. And budget headroom over the loaded set.
p_ld_budget: assert property (@(posedge clk) disable iff (!rst_n)
  load_accepted |-> !budget_exceeded);

// C4. And a free slot.
p_ld_slot: assert property (@(posedge clk) disable iff (!rst_n)
  load_accepted |-> (slots_used < 3'(SLOTS)));

// C5. The load counter advances on every accepted load.
p_ld_count: assert property (@(posedge clk) disable iff (!rst_n)
  load_accepted |=> (c_loads_since_verification ==
                     $past(c_loads_since_verification) + 1));

// C6. An unload frees exactly one slot.
p_ld_unload: assert property (@(posedge clk) disable iff (!rst_n)
  unload_req |=> (slots_used == $past(slots_used) - 1));

Group D — the verifier.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// D1. The verdict is the conjunction of the three analyses.
p_vf_verdict: assert property (@(posedge clk) disable iff (!rst_n)
  verdict_ok == (!fails_termination && !fails_budget && !fails_memory
                 && !fails_signature));

// D2. An unbounded loop fails termination.
p_vf_loops: assert property (@(posedge clk) disable iff (!rst_n)
  (unbounded_loops > MAX_LOOPS) |-> fails_termination);

// D3. A worst-case path over the budget fails the budget check.
p_vf_budget: assert property (@(posedge clk) disable iff (!rst_n)
  (worst_case_path > MAX_INSTR) |-> fails_budget);

// D4. An out-of-region offset fails memory safety.
p_vf_memory: assert property (@(posedge clk) disable iff (!rst_n)
  (max_memory_offset >= program_region_size) |-> fails_memory);

// D5. A closed domain requires a full pass.
p_vf_closed: assert property (@(posedge clk) disable iff (!rst_n)
  domain_is_closed |-> verdict_ok);

// D6. Verified plus rejected equals verification requests.
p_vf_account: assert property (@(posedge clk) disable iff (!rst_n)
  verify_req |=> ((c_verified + c_rejected) ==
                  ($past(c_verified) + $past(c_rejected) + 1)));

Group E — determinism.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// E1. A packet never exceeds its program's declared worst case.
p_dt_worst: assert property (@(posedge clk) disable iff (!rst_n)
  pkt_done |-> (instructions_taken <= program_worst_case));

// E2. The maximum is monotone.
p_dt_max: assert property (@(posedge clk) disable iff (!rst_n)
  max_instructions >= $past(max_instructions));

// E3. The minimum never exceeds the maximum.
p_dt_order: assert property (@(posedge clk) disable iff (!rst_n)
  min_instructions <= max_instructions);

// E4. The part is content-dependent and says so.
p_dt_declares: assert property (@(posedge clk) disable iff (!rst_n)
  content_dependent);

// E5. Over-budget packets are counted, not silently truncated.
p_dt_counted: assert property (@(posedge clk) disable iff (!rst_n)
  (pkt_done && (instructions_taken > budget)) |=>
    (c_over_budget == $past(c_over_budget) + 1));

// E6. The adversary flag is the arithmetic and nothing more.
p_dt_adversary: assert property (@(posedge clk) disable iff (!rst_n)
  adversary_can_halve == (max_instructions > (budget * 16'd2)));

Group F — the claim.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// F1. A conformance claim requires every resident program verified.
p_cf_domain: assert property (@(posedge clk) disable iff (!rst_n)
  claim_conformance |-> behaviour_is_verified);

// F2. An unverified resident clears the flag.
p_cf_unverified: assert property (@(posedge clk) disable iff (!rst_n)
  unverified_resident |-> !behaviour_is_verified);

// F3. A throughput claim names a worst case, not a mean.
p_cf_throughput: assert property (@(posedge clk) disable iff (!rst_n)
  claim_line_rate |-> fits_at_worst);

// F4. The load count since verification is exposed.
p_cf_exposed: assert property (@(posedge clk) disable iff (!rst_n)
  snapshot |=> (o_loads_since_verification ==
                $past(i_loads_since_verification)));

// F5. The silicon's properties hold regardless of the program.
p_cf_silicon: assert property (@(posedge clk) disable iff (!rst_n)
  pkt_arrives |-> (accepted || array_saturated));

Thirty-four properties. Every one is about the silicon, and the silicon is the same after a program load as before it.


21. Verification Scenarios

Fifty-eight scenarios. Group 4's cannot be run before the programs exist, which is the chapter's problem stated as a test plan.

Group 1 — the instruction budget (10).

#ScenarioExpect
18 cores, 1.5 GHz, 1 IPC, 148.81 Mpps80.6 instructions per packet
216 cores161.3
364 cores645.1
464 cores, 2 GHz, 2 IPC1 720.3
5a 100-instruction program9.9 cores needed
6a 1 000-instruction program99.2 cores
7a 2 000-instruction program198.4 cores — 243 datapaths
8one 3 GHz host core at 3 IPC60.5 instructions per packet
9the 64-core array against a host10.7 host cores
10budget_per_packet with pps_million = 0saturates; no divide by zero

Group 2 — what fits (8).

#ScenarioExpect
11parse + two lookups + rewrite + state update200–390 instructions — fits
12a full TCP state machine1 000–3 000 — does not fit
13AES-GCM over 64 octets at 2 per instruction32 — fits
14the same over 1 500 octets750 — over the whole budget
15the same over 9 000 octets4 500 — 7× over
16touches_every_octet assertedmust_be_fixed — the comparator refuses
17a general-purpose ISA doing row 11's work2–4× the instructions
18a fixed AES engine insteadone packet per cycle, a few hundred thousand BCE

Group 3 — the array (10).

#ScenarioExpect
19uniform 200-instruction packets, run to completionutilisation ≈ 30%; no rejections
20uniform 645-instruction packetsutilisation ≈ 100%; at the edge
21a burst of 1 200-instruction packets64 cores held 800 ns; arrivals rejected
22mixed 200/1 200 with run-to-completionballs in bins — Chapter 23.2 §10's distribution
23the same, pipelined insteadbetter utilisation, worse locality
24three table misses at 200 cycles each600 of a 645 budget spent stalled
25the same with 4-way multithreading≈4× the utilisation for the same cores
26acceptance with every core busyp_ar_free fires
27accepted and saturated togetherp_ar_exclusive fires
28an arrival neither accepted nor rejectedp_ar_accounted fires

Group 4 — programs, which cannot be enumerated (12).

#ScenarioExpect
29load a signed, verified, in-budget programaccepted
30load an unsigned programrejected — C1
31load a program with an unbounded loopverifier fails termination
32load one whose worst path is 700 against 645verifier fails the budget
33load one reading outside its regionverifier fails memory safety
34load two programs of 400 each800 against 645 — budget_exceeded
35a program whose declared worst case understates the real oneadmitted; the array saturates; E1 fires late
36the same with a run-time budget capthe packet is killed and counted — bounded
37unload a slot with a packet mid-programa mixture of two programs — undefined
38the same with a valid bit and a drainbounded by 800 ns; defined
39forty loads since verificationc_loads_since_verification = 40
40a device-level invariant proved on programs 1–5holds; says nothing about program 6

Group 5 — determinism and the adversary (10).

#ScenarioExpect
41a program with mean 400, worst 1 200240 Mpps at the mean, 80 at the worst
42the same as a fraction of line rate161% and 53.8%
43traffic that always takes the short paththe benchmark figure
44traffic constructed to take the long path53.8% — a denial of service with valid frames
45adversary_can_halve with worst = 1 200, budget 645asserts
46adversary_can_halve with worst = 700does not assert
47the spread ratio at min 200, max 1 2006.0× — the C_SPREAD caveat
48eight programs, one long-pathno per-program attribution without extrema
49the same with per-program extremaprogram 5 named, 0.3% of packets
50a packet exceeding its declared worst casep_dt_worst fires

Group 6 — claims and telemetry (8).

#ScenarioExpect
51a conformance claim with an unverified residentp_cf_domain fires
52a line-rate claim against the meanp_cf_throughput fires — it must be the worst case
53a 32-bit packet counter at 148.81 Mppswraps in 28.9 s
54a program-set hash across a load and an unloadchanges on both
55the same configuration reloadedthe hash returns to its previous value
56an auditor comparing against an approved hasha yes or no answer
57domain_is_closed falseevery device-level property is inapplicable, and says so
58the array's control logic total0.15 datapaths against an array of 78.6

Scenario 40 is the one that cannot be run and scenario 57 is the one that makes its absence visible.

The directed test random stimulus will not produce

This test requires a device-level invariant to hold through a full verification campaign and then be falsified by a program that is signed, terminating, in budget and memory-safe.

The arrangement:

StepAction
1a device with a verified invariant: packets within a flow are emitted in order
2programs 1 to 5 are written, verified and run; the invariant holds in every test
3the part ships; the verification campaign is complete and signed off
4program 6 is written eighteen months later by a different team
5it passes every load-time check — signature, termination, budget, memory bounds
6it reorders packets within a flow, deliberately, as part of a load-balancing feature

Random stimulus will not produce this because the stimulus space a generator explores is packets, and the thing that changes the device's behaviour is a program. A constrained-random campaign of any length, against programs 1 to 5, explores the packet space exhaustively and the program space not at all — and the program space is where the failure lives. Generating random programs is possible and does not help either, because the interesting program is not random: it is a deliberate, sensible feature that happens to violate an invariant nobody told its author about.

The oracle is in four parts and the first three are expected passes.

PartSignalRequired valueWhy alone it is not enough
1 — the campaign passesthe invariant, programs 1–5holds throughoutit is true; that is the problem
2 — program 6 passes every load checksignature, termination, budget, memoryall passit is a legitimate program
3 — the silicon's properties holdSection 20's groups A–Eall 34nothing in the hardware is wrong
4 — the invariant is falsethe reorder check, with program 6 loadedfailsthis is the finding

Part 2 is the assertion worth writing and it asserts that something passes. Program 6 is not malformed, not hostile and not a bug — it is a feature, correctly implemented, that violates a device-level property its author never saw. A test suite that only checks for failures cannot express this, and a verification plan that treats a passing load check as evidence about behaviour has made exactly the inference Section 12 refuses.

Part 4 needs the invariant to still be under test eighteen months after the campaign closed, which is the real recommendation: device-level invariants must be re-run at every program load, in the field, as part of the load path — or stated in the programming model so that program 6's author is told. The second is cheaper and is what a good SmartNIC's documentation does; the first is what a device with a conformance obligation must actually do.


22. Debugging a SmartNIC

A procedure, and the first question is one no earlier chapter had to ask.

StepDoBecause
1ask which programs are loadedthe device's behaviour is a function of them
2read c_loads_since_verification and the program-set hashis this the configuration anyone examined?
3read the instruction spread — min, mean, maxSection 13 — the worst case is what throughput follows
4read the per-program extremawhich program has the long path
5read the over-budget and rejection countsthe array saturating, and why
6read core utilisation against the mean instruction counta gap means stalls, not shortage
7check whether the traffic is steering onto a long pathSection 13 — a denial of service with valid frames
8only now look at the link, the host and the queuesChapter 23.4's space, unchanged

Step 1 first is the whole difference between this part and every other one in the track. For a MAC, a PHY, a switch or a fixed-function NIC, "what is it doing" has one answer for the life of the part. Here it has as many answers as the device has had loads, and the debugging session cannot start until that answer is known.

Four signatures.

SignatureCause
throughput at 53.8% on some traffic and 100% on other traffica long program path selected by packet content — Section 13
core utilisation high, instruction count lowmemory stalls, not instruction shortage — Section 5
rejections with most cores idlerun-to-completion holding a few cores on long packets
an invariant that held in the lab and does not in the fielda program loaded after the campaign — Section 20

Row two is the one that misleads an area decision. A utilisation figure of 90% suggests the array is the bottleneck and more cores are the answer; if the cores are stalled on table lookups, more cores stall too and the answer is multithreading or a faster table. Distinguishing them takes the instruction count alongside the utilisation: 90% utilisation at 200 instructions per packet means the cores are waiting, not working.

Row four is the chapter's own failure mode and it has a procedure rather than a fix. The invariant did not break — the device stopped being the device the invariant was proved about. The question is not what is wrong with the hardware; it is which load changed the behaviour, which c_loads_since_verification and the program-set hash answer in one read.


23. Misconceptions

Six, in wrong-model / what-it-costs / corrected-model form.

Misconception 1 — "programmability is cheap."

Wrong model: a few small cores instead of a few fixed blocks; roughly a wash.

What it costs: a factor of 197. Section 6: Chapter 18.7 §18's fixed-function offload set is 5 650 flops — 113 000 BCE, 0.40 of a MAC receive datapath. A 64-core array that replaces it is 2.23 × 10⁷ BCE, 78.6 datapaths, and it makes Chapter 23.4's NIC 44% larger.

Corrected model: and two thirds of the cost is instruction memory rather than cores — 59.2 datapaths against 19.4. A programmable array is mostly a place to keep programs. Reducing the per-core instruction memory from 32 kB to 8 kB saves 57% of the array, which is the largest single lever available and is rarely the one people reach for.

Misconception 2 — "a SmartNIC can do anything a host can."

Wrong model: it has processors, so it is a computer.

What it costs: an architecture built on a budget that does not exist. Section 2: 645 instructions per packet at 64 cores and 1.5 GHz. A full TCP state machine is 1 000 to 3 000 per segment; AES-GCM over a 1 500-octet payload at two octets per instruction is 750 — more than the whole budget for one packet.

Corrected model: the budget is per packet and a payload cost is per octet, so the two scale differently and no core count reconciles them. Programmable cores make decisions about headers; fixed-function engines transform payloads — which is why every real SmartNIC is an array with accelerators bolted to it rather than a general-purpose processor.

Misconception 3 — "measure it at the mean instruction count."

Wrong model: a program averages 400 instructions, so size the array for 400.

What it costs: 46% of line rate on the traffic that matters. Section 13: a mean of 400 sustains 240 Mpps and a worst case of 1 200 sustains 80 — 53.8% of line rate — and which packets take the long path is decided by packet content.

Corrected model: a programmable datapath's throughput follows its worst path, not its mean, and the worst path is reachable by anyone who can send packets. If the worst case is more than twice the budget, a sender who can steer traffic onto it halves the part's throughput — with valid frames, no protocol violation and nothing a firewall would notice.

Misconception 4 — "the verifier makes the device safe."

Wrong model: load-time checks establish that the program is acceptable.

What it costs: a misplaced trust boundary. Section 12: the verifier establishes provenance, termination, an instruction budget and memory safety — all decidable, all cheap, all genuine. It establishes nothing about behaviour. A program that drops one tenant's traffic, or mirrors it elsewhere, or rewrites a header to bypass a policy is terminating, in budget and memory-safe.

Corrected model: the verifier establishes that the program is inside the domain the device's properties were written over — it stops the program breaking the device, not the program doing the wrong thing. The behavioural trust is the signature, so the whole security argument reduces to key management, which is a different discipline and the one that actually matters.

Misconception 5 — "we verified the device."

Wrong model: the verification campaign closed, the part shipped, the behaviour is known.

What it costs: the standing of the campaign. Section 10: the device's behaviour is a function of the silicon and a program, and the programs the campaign ran were the ones that existed then. A program written eighteen months later by a different team can pass every load-time check and falsify a device-level invariant — Section 21's directed test — and there is no event, no counter and no assertion failure at the moment it happens.

Corrected model: what was verified is the silicon, plus the programs examined. Closing the gap needs a mechanism rather than a stronger property: a verifier in the load path that refuses anything outside the domain, and device-level invariants re-run at every load or stated in the programming model so the next author is told. And c_loads_since_verification is the honest summary of where a device stands.

Misconception 6 — "programmability makes the part future-proof."

Wrong model: a changeable function means the part adapts to whatever comes.

What it costs: the distinction between what is changeable and what is not. The function is changeable; the instruction budget is not. Sixty-four cores at 1.5 GHz give 645 instructions per packet and that number is fixed at tape-out by the core count and the clock.

Corrected model: a future requirement that needs 1 500 instructions per packet is as unreachable on this SmartNIC as on a fixed-function part — it needs 149 cores, 243 datapaths, and a different die. Programmability accommodates change in the function; it does not accommodate growth in the budget, and requirements usually grow in the second direction.


24. Interview Questions

Six, with what a strong answer contains.

1. What is a SmartNIC's per-packet instruction budget, and where does it come from?

Cores × frequency × IPC ÷ packet rate. At 64 cores, 1.5 GHz, one instruction per cycle and 148.81 Mpps that is 645 instructions per packet. A strong answer reads it the other way too — a 500-instruction program needs 49.6 cores, a 1 000-instruction one needs 99.2 — and names the comparison that sells the part: a 3 GHz host core at 3 IPC gets 60.5 instructions per packet, so the array is worth 10.7 host cores.

2. Why is every SmartNIC an array with accelerators rather than just an array?

Because the budget is per packet and a payload cost is per octet. AES-GCM over a 1 500-octet payload at two octets per instruction is 750 instructions — more than the whole 64-core budget for one packet — and over 9 000 octets it is 4 500, seven times over. A strong answer states the rule: anything that touches every byte must be fixed function; anything that touches a header can be a program, and no core count changes that because the two costs scale differently.

3. What does programmability cost against the blocks it replaces?

197×. Chapter 18.7 §18's fixed-function set is 5 650 flops — 113 000 BCE, 0.40 datapaths; a 64-core array is 2.23 × 10⁷ BCE, 78.6 datapaths, making Chapter 23.4's NIC 44% larger. A strong answer adds the breakdown — cores 19.4 datapaths, instruction memory 59.2 — and the lever: halving the per-core instruction memory saves more than the cores cost.

4. Why does a programmable datapath's throughput depend on packet content?

Because the program's path length does. A mean of 400 instructions sustains 240 Mpps and a worst case of 1 200 sustains 80 — 53.8% of line rate — and which path a packet takes is decided by its own contents. A strong answer draws the security conclusion: if the worst case exceeds twice the budget, a sender who can steer packets onto it halves the part's throughput, using valid frames and violating nothing — a denial of service that no firewall would see.

5. What can a load-time verifier establish?

Provenance, termination, an instruction budget and memory safety — and nothing about behaviour. Termination is decidable only because the language forbids unbounded loops; the budget is a longest-path walk over an acyclic control-flow graph; memory safety is an offset bound. A strong answer names the boundary: a program that drops one tenant's traffic is terminating, in budget and memory-safe, so the verifier admits it correctly — and the whole behavioural trust is the signature, which makes the security argument key management.

6. You verified the device and shipped it. What has your verification established?

That the silicon behaves, and that the programs you ran behave. The device's behaviour is a function of silicon plus a program, and the deployed program set is open. A strong answer names the shape of the fix: not a stronger property but a mechanism — a verifier in the load path that refuses anything outside the domain — and the honest summary a device should report: c_loads_since_verification, which is how many behaviours the part has had since anybody examined one.


25. Questions and Answers


26. What's Next

Three parts of Module 23 are done — the fabric, the switch and the two NICs. One remains, and it is the one where the designer holds the silicon.

This chapter established
the budget645 instructions per packet at 64 cores and 1.5 GHz
what it is worth10.7 host cores; 4.1 to 7.9 freed by a realistic function set
what it costs78.6 datapaths — 197× the fixed-function set, +44% on the NIC
where the cost istwo thirds instruction memory, one third cores
what programs cannot doanything per-octet — 750 instructions for one AES pass
what content-dependence costs53.8% of line rate on the long path, and it is reachable
the refused propertyan invariant proved over the programs that existed then

And the chapter's distinctive contribution to the track's argument is in its replacement property. Nine previous chapters replaced a bad property with a measurement, an imported number or a separate quantity. This one replaces it with a refusal — a gate in the load path — which is the first prevention mechanism the track has needed, and it exists because the gap it closes is not an observation problem but a domain problem.

Chapter 23.6 builds the whole thing in an FPGA. Everything in Modules 19 to 23 has been priced in a unit that assumes a fixed process and an ASIC's freedom to place anything anywhere; an FPGA offers neither. Its resources are a fixed inventory of lookup tables, flip-flops, block RAMs and DSP slices, laid out in advance, and a design either fits or does not. The questions are what a MAC and a PHY actually consume in that inventory, how Chapter 23.3 §2's area unit translates when memory comes in fixed-size blocks, what the achievable clock frequency does to every rate in the track, and what happens to a timing-derived property when the frequency it assumed is an output of the implementation flow rather than an input to it.

Continue learning

Standards & specifications

Governing standard
IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)

Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Ethernet curriculum.