Benchmarks

Latest run: 2026-11-11. Board: AMD Kria KR260 (XCK26, Zynq UltraScale+). Sender: Linux workstation, Intel E810, 3 m passive DAC into the KR260 SFP+ cage. Test feed: a 20-frame, 659-message Cboe PITCH capture (14 message types, about 33 messages per 1028-byte frame). You can repeat the correctness and latency runs yourself on the same board from the Run on Hardware page. Source and tooling: github.com/fpganow/Market.Data.PoC.

BATS parser on the Kria KR260: all implementations

Three implementations of the parser are installed on the board and selectable on the live page. All three were run on 2026-11-11 over the 10G cable. Latency is parse time minus receive time, both stamped in the fabric, for every message, with the records read out by the host.

LabVIEW FPGA IP export (2026-10-20)Verilog, LabVIEW IP interface (2026-10-20)Pure Verilog (build of 2026-11-11)
Parser clock100 MHz100 MHz156.25 MHz (the 10G receive clock)
Messages field-exact659 / 659659 / 659659 / 659
Latency, median685 ns326 ns410 ns
Latency, 99th percentile1.32 µs582 ns800 ns
Latency, max1.36 µs589 ns0.81 µs
Throughput with in-fabric countersnot measured for this export (an earlier export held 1.5 Gbit/s, see below)not measured (this build has no counters)0 loss at every rate one sender can reach (3.9 to 4.8 Gbit/s); 160 000-frame burst from four senders (9.8 Gbit/s combined), 5 272 000 records, 0 lost

Notes: on the LabVIEW export, 3 of the 659 records carry no receive timestamp and are left out of its latency figures. The pure Verilog bitstream was rebuilt on 2026-11-11 with a 1 024-record output FIFO (it was 16): the earlier build stalled behind the slow host readout, which showed up on the live page as latencies of up to 294 µs and lost records at faster playback speeds. The parser itself is unchanged, and both the page and the in-fabric counters now read 0.01 to 0.81 µs at every playback speed.

In-fabric comparison

These numbers come from counters inside the fabric, not from the host, so they measure the parser and nothing else. The LabVIEW column is the 2026-09-23 export measured on 2026-11-08; the Verilog rate sweep was repeated on 2026-11-11.

LabVIEW FPGA IP (mktdata_poc)Pure Verilog (mktdata_rtl)
Clean throughput1.5 Gbit/s = 6.0 M msg/s10G line rate: 160 000-frame burst, 5 272 000 records, 0 lost
First failure2 Gbit/s (desync)none found; sustained tests limited by the sender at 4.75 Gbit/s
Max latency, last message of a 1 kB frame1.3 µs at 100 Mbit/s, 10 µs at 1 Gbit/s0.81 µs (the frame’s own arrival time at 10G)
Cost per message6 to 9 cycles at 100 MHz + 160 ns record serialisation1 cycle at 156.25 MHz (6.4 ns)
Parser fabric11.3k LUTs, 4.3k FFs, 92 BRAM2.3k LUTs, 2.1k FFs, 0 BRAM
Timing100 MHz, closes only with Performance_ExplorePostRoutePhysOpt156.25 MHz, WNS +0.108 ns

LabVIEW FPGA IP export, rate sweep

Each row is 20 passes of the capture at the offered rate (13 180 messages). “Desync” means the parser numbered more messages than were sent: it lost bytes mid-frame and resynchronised at the next frame start.

Offeredmsg/sRecordsSeq gapsMax latencyVerdict
1 Mbit/s to 1.5 Gbit/sup to 6.0 M13 18001.3 to 10 µs100 %
2 Gbit/s8.0 M12 4962 28712 µsdesync (numbered 14 783)
3 Gbit/s12.0 M9 4255 01712 µsdesync
4 Gbit/s16.0 M8 4164 7628 µs36 % numbered, never output

The ceiling is the output record: 16 words of 64 bits, written one word per 100 MHz cycle, is 160 ns per message, or 6.25 M msg/s. The knee sits exactly there (6.0 M passes, 8.0 M fails). The parser state machine itself, at 6 to 9 cycles per message, would allow 11 to 16 M msg/s.

Pure Verilog parser

TestResult
Rate sweep, 100 Mbit/s up to the sender limit (3.9 to 4.8 Gbit/s), 20 passes each, 2026-11-11400 / 400 frames, 13 180 / 13 180 records, 0 gaps, 0 stall cycles, max latency 0.81 µs at every rate
Four parallel senders at top speed, 160 000 frames, wire saturated per host NIC counters160 000 frames received, 5 272 000 contiguous records, 0 gaps, 0 drops, 0 truncated, max latency 0.81 µs
Vivado simulation, 20-frame capture659 / 659 records, every field correct

The parser consumes one 64-bit word per 156.25 MHz clock, the XGMII data rate, and emits one message per clock, so it cannot fall behind the wire. It is cut-through: the first message of a frame is out about 40 ns (6 clocks) after its bytes land, while the rest of the frame is still arriving; the 0.81 µs figure is the last message of a 1012-byte frame and equals the frame’s own arrival time.

What is not yet measured

  • Sustained minutes at 10G. The Python senders hold about 4.75 Gbit/s on their own; the 10G result is a 1.3-second burst from four of them. A pktgen or C sender is the next step.
  • Line rate into software. Both parsers hand records to the CPU through a 32-bit AXI-Lite FIFO read by a polling process, about 8 000 records/s. At line rate the records are parsed and then dropped at that FIFO. A DMA ring into DDR4 is the planned fix.
  • Wire-to-application latency through that readout.

For scale

Cboe publishes, for its busiest equities feed (EDGX), 2.14 M msg/s sustained one-second peaks and about 26 M msg/s in a 1 ms microburst; the equities feeds are shaped to 1 Gbit/s. The LabVIEW IP already covers a gigabit-shaped feed; the Verilog parser covers the 10G line-rate goal and the options feeds.

How the numbers are taken

  • An AXI-Stream monitor in the fabric (axis_mon) counts records, sequence gaps, stall cycles and received frames, and records min/max receive-to-parse latency. Its “sink” mode consumes the records in fabric so the host readout is out of the loop.
  • The sender is scripts/send_pcap_udp.py (pps, Mbit/s, pcap-timestamp replay or top speed); scripts/rate_sweep.py --sink runs the sweep and reads the counters after each rate.
  • Correctness is scored by ip_export/netlist_sim/eval_sim.py, which checks every field of every record against the capture.
  • Timestamps: the LabVIEW designs count the 156.25 MHz MAC clock (6.4 ns per tick); the Verilog parser keeps a true 10 ns tick. LabVIEW latencies printed by the 2026-11-08 sweep as 10 ns ticks are shown here converted to 6.4 ns ticks. Figures published before 2026-10-11 used 10 ns per tick and are 1.56× too large.

History

DateChange
2026-11-11All three installed implementations re-run through the live page: 659/659 each. Pure Verilog bitstream rebuilt with a deeper output FIFO; rate sweep and 160 000-frame burst repeated on it: 0 loss, 0.81 µs. Playback speed added to the live page.
2026-11-08Pure Verilog parser: 10G burst with 0 loss, 0.81 µs. LabVIEW IP ceiling measured at 1.5 Gbit/s with the new in-fabric counters.
2026-11-06First run over a real 10G cable from the Linux workstation: 659/659 field-exact. Host readout found to cap the system at 3 Mbit/s offered rate.
2026-09-22Frame-boundary corruption traced to the vendor IPv4/UDP library; fixed and covered by a regression capture.