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 clock | 100 MHz | 100 MHz | 156.25 MHz (the 10G receive clock) |
| Messages field-exact | 659 / 659 | 659 / 659 | 659 / 659 |
| Latency, median | 685 ns | 326 ns | 410 ns |
| Latency, 99th percentile | 1.32 µs | 582 ns | 800 ns |
| Latency, max | 1.36 µs | 589 ns | 0.81 µs |
| Throughput with in-fabric counters | not 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 throughput | 1.5 Gbit/s = 6.0 M msg/s | 10G line rate: 160 000-frame burst, 5 272 000 records, 0 lost |
| First failure | 2 Gbit/s (desync) | none found; sustained tests limited by the sender at 4.75 Gbit/s |
| Max latency, last message of a 1 kB frame | 1.3 µs at 100 Mbit/s, 10 µs at 1 Gbit/s | 0.81 µs (the frame’s own arrival time at 10G) |
| Cost per message | 6 to 9 cycles at 100 MHz + 160 ns record serialisation | 1 cycle at 156.25 MHz (6.4 ns) |
| Parser fabric | 11.3k LUTs, 4.3k FFs, 92 BRAM | 2.3k LUTs, 2.1k FFs, 0 BRAM |
| Timing | 100 MHz, closes only with Performance_ExplorePostRoutePhysOpt | 156.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.
| Offered | msg/s | Records | Seq gaps | Max latency | Verdict |
|---|---|---|---|---|---|
| 1 Mbit/s to 1.5 Gbit/s | up to 6.0 M | 13 180 | 0 | 1.3 to 10 µs | 100 % |
| 2 Gbit/s | 8.0 M | 12 496 | 2 287 | 12 µs | desync (numbered 14 783) |
| 3 Gbit/s | 12.0 M | 9 425 | 5 017 | 12 µs | desync |
| 4 Gbit/s | 16.0 M | 8 416 | 4 762 | 8 µs | 36 % 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
| Test | Result |
|---|---|
| Rate sweep, 100 Mbit/s up to the sender limit (3.9 to 4.8 Gbit/s), 20 passes each, 2026-11-11 | 400 / 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 counters | 160 000 frames received, 5 272 000 contiguous records, 0 gaps, 0 drops, 0 truncated, max latency 0.81 µs |
| Vivado simulation, 20-frame capture | 659 / 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 --sinkruns 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
| Date | Change |
|---|---|
| 2026-11-11 | All 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-08 | Pure 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-06 | First 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-22 | Frame-boundary corruption traced to the vendor IPv4/UDP library; fixed and covered by a regression capture. |