{'fraction_non_alphanumeric': 0.03256713031054335, 'fraction_numerical': 0.01453964785477161, 'mean_word_length': 4.760711692084241, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 13, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
The nanoPU: Redesigning the CPU-Network Interface to Minimize RPC Tail Latency

The nanoPU is a new networking-optimized CPU designed to minimize tail latency for RPCs. By bypassing the cache and memory hierarchy, the nanoPU directly places arriving messages into the CPU register file. The wire-to-wire latency through the application is just 65ns, about 13x faster than the current state-of-the-art. The nanoPU moves key functions from software to hardware: reliable network transport, congestion control, core selection, and thread scheduling. It also supports a unique feature to bound the tail latency experienced by high-priority applications. Our prototype nanoPU is based on a modified RISC-V CPU; we evaluate its performance using cycle-accurate simulations of 324 cores on AWS FPGAs, including real applications (MICA and chain replication).


Introduction
"Sustaining exponential scaling in our computing infrastructure requires much more predictable, low-latency, CPU-efficient communication frameworks starting with RPCs, rather than IP packets." Amin Vahdat, 2020 Cloud service providers (CSPs) are trying to drive down RPC tail latency as more applications are deployed using the microservices architecture [4]. New distributed applications are demonstrating extraordinary performance running on commodity servers in cloud data centers (e.g., video encoding [19], video compression [2], and face recognition [10]) by dividing computation into fine-grained tasks that execute simultaneously. These applications typically fanout Remote Procedure Call (RPC) requests from a root to a large number of leaves, and in multiple tiers. Most often, the service-level performance is limited by the RPC tail latency of individual leaves [17]. Therefore, if we can reduce (or even bound) RPC tail latency, distributed applications will run faster.
Modern CSPs are attempting to tackle this problem by introducing specialized NIC hardware [6,38] with fast RDMA and NIC-resident CPU cores running low-latency microservices. As a rough rule of thumb, a microservice takes 5-10µs to invoke and therefore is only worth invoking if we send it for more than 10µs of computation [27,41,28]. By comparison, the goal of our work is to enable efficient sub-microsecond RPCs that can be invoked with under 1µs of communication overhead at the server. One of the key metrics we use in this paper is the wire-to-wire latency, defined as the time from when the first bit of an RPC request message arrives at the NIC, until the first bit of the processed RPC response leaves the NIC. The best reported median wire-to-wire latency is around 850ns [28]. Our goal is to reduce both median and tail numbers to below 100ns, making it worthwhile to run "nanoServices"; short RPCs requiring less than 1µs of work.
Many prior attempts to reduce RPC overhead have included low-latency and lossless switches [30,35,8], a reduced number of network tiers, and specialized libraries [28]. The current fastest approaches deploy dedicated NIC and switch hardware, but these are hard to program [25,24,26,50].
Our work asks the question: Can we design a future CPU core that is easy to program, yet can serve RPC requests with the absolute minimum overhead and tail latency? Our design, which we call the nanoPU, can be seen as a model for future CPU cores optimized for sub-microsecond RPC service, in addition to their regular processing. Alternatively, the nanoPU can be thought of as a new class of domain-specific nanoService processor, designed to sit on a smartNIC or as a standalone cluster to serve sub-microsecond RPCs. For example, it would be practical today to build a single chip 512-core nanoPU, similar to Celerity [15], with one hundred 100GE interfaces, servicing over 500 million RPCs per second at a sustained 10Tb/s. Such a device could radically improve the performance of large distributed applications.
Our approach is based on four key observations: First, we need to minimize the time from when an RPC request packet arrives over Ethernet until it starts processing in a running thread. The nanoPU does this by replacing the software threadscheduler and core-selector (aka load-balancer) with hardware; by bypassing PCIe, main memory and cache hierarchy completely; by placing RPC data directly into the CPU register file; and by replacing the host networking software stack with a reliable transport layer in hardware, delivering complete RPC messages to the CPU. Second, we need to minimize network congestion. The nanoPU implements NDP [20] in hardware (using a programmable P4 pipeline [23]), reducing congestion and improving incast performance. Third, we need to maximize RPC throughput by pipelining header and transport layer processing, thread scheduling and core-selection in hardware. The nanoPU includes a P4 PISA pipeline [9] in the NIC, processing several packets in parallel, and reassembling RPC messages. Finally, the performance of large distributed applications is often limited by RPC tail latency; we therefore need to tame and minimize tail latency when processing RPCs.
The nanoPU provides what we believe to be the first bounded tail latency RPC service, guaranteeing that a conforming RPC request will complete service within, for example, 1µs of its arrival at the NIC.
1.1. Causes of high RPC tail latency a. Memory and cache hierarchy on the critical path. The networking stack of a modern CPU uses memory as a workspace to hold and process packets. This inherently leads to interference with applications' memory accesses, introducing resource contention which causes poor RPC tail latency. Furthermore, if a packet is transferred over PCIe to DRAM, it is not available to the CPU until several hundred nanoseconds after it arrived [42]. With direct-cache access technologies (like DDIO [16] and DCA [21]), this is reduced, but the packet must still go through many layers of the networking stack, with additional latency for context switching (1-5µs) [27,14], including memory copies, TLB flushes, virtual memory management, and cache replacement.
b. Suboptimal scheduling. When an RPC packet arrives, it must be dispatched to a core for network-stack processing; when complete, the RPC request message is forwarded to a worker core for processing. At each step, a software coreselection algorithm 1 selects the core; and a thread scheduler decides when processing begins. Both algorithms require frequent access to memory by the cores and the NIC, requiring mediation of the memory bus, PCIe, and cache lines. Others have shown that these algorithms are on the critical processing path and have attempted to drive down the processing time [41,27]. However, the granularity of these software schedulers is inherently limited by the overhead required to perform intercore synchronization (e.g. sending and receiving interrupts). Hence, it becomes impractical to make scheduling decisions more than once every 5µs.
While we are not the first to try and reduce the latency of these processing steps [42,27,41,28,12,47], we believe this paper describes the first complete design to minimize RPC tail latency. The nanoPU aims to minimize the time spent in every layer, with the judicious use of pipelined, programmable hardware, and direct placement of data into the CPU corebypassing the memory and cache hierarchy completely.
Our contributions: In summary, we make the following research contributions: • The nanoPU: A novel co-design of the NIC and CPU to minimize RPC tail latency. Our design includes: (1) a dedicated memory hierarchy in the NIC, connected directly to the CPU register file, (2) low-latency hardware transport logic, core selection, and thread scheduling, and (3) bounded tail latency by restricting message processing time. • An open-source prototype of nanoPU 2 extending the RISC-V Rocket core [46] with a 200Gb/s network interface, evaluated with reproducible cycle-accurate simulations on AWS F1 FPGA instances. • Demonstrated (1) wire-to-wire latency of just 65ns (13ns without the Ethernet MAC and serial I/0), 13× faster than the best reported results, (2) 200Gb/s throughput per core, 2.5× faster than the state of the art, (3) 350 Mpkts/s processing in the NIC (including transport and core selection logic), 50× faster than the Shinjuku [27], Shenango [41] and eRPC [28] software solutions, (4) hardware preemptive thread scheduling that enables 99% tail latency below 2.1µs under high load, (5) the first system to deterministically bound RPC tail latency, and (6) efficient core-selection algorithm in hardware. • Hardware implementation of reliable, low-latency NDP [20] transport layer and congestion control. To our knowledge, it is the first end-to-end evaluation of a hardware transport protocol implemented at an academic institution. • We demonstrate a key-value store (MICA [34]) storing 3way replicated writes in 1.1µs (excluding switch latency), 8× faster than the state of the art [26]. Figure 1 is a block diagram of the nanoPU. Here, we describe each architectural block in turn.

Hardware Terminated Transport Logic
The transport logic performs three main pipelined tasks in hardware: (1) it processes packet headers, such as VXLAN, overlay tunnels, encryption and decapsulation, (2) converts between Ethernet frames and application messages (e.g., RPC requests and responses), and (3) performs congestion control to reduce in-network latency. The nanoPU transport logic provides the abstraction of reliable one-way message delivery to applications, as opposed to reliable, bi-directional byte stream provided by TCP. Reliable one-way message delivery can be efficiently implemented in hardware: it only needs to maintain per-message state, rather than per-connection state, so the state can be freed once the message has been successfully delivered to the destination (either a remote host or a local core). Per-message state requirements are small; beyond storing the actual message, we keep a per-message bitmap of received packets, and a few bytes for congestion control. The number of simultaneous RPC communications scales linearly with the number of outstanding messages, rather than the number of hosts in a datacenter, allowing large scale, highly distributed applications involving thousands of nanoPUs. We are not the first to suggest a reliable message abstraction at the transport layer [37,20], but we believe we are the first to place it in programmable hardware, allowing multiple transport algorithms to be compared.
A hardware transport layer can be heavily pipelined, allowing it to process several packets at the same time, and freeing the CPU to focus on what it should be running-application  Figure 1: The nanoPU design. The NIC includes ingress and egress PISA pipelines as well as a hardware-terminated transport and a core selector with global RX queues; each CPU core is augmented with a hardware thread scheduler and local RX/TX queues connected directly to the register file.
code. As a secondary benefit, by implementing the transport logic in a fixed latency hardware pipeline, the tail latency of processing each packet is significantly lower than the same algorithm running in software. Furthermore, a hardware transport layer responds faster than software, leading to a tighter congestion control loop between end-points, and hence more efficient use of the network.
Implementing programmable transport logic in hardware requires support for the following functions in the NIC: • Packetization/retransmission buffer to break a message into packets, and to store outgoing packets until they are acknowledged by a receiver. • Reassembly buffer to handle out-of-order packets.
• Timers and timer-based event processing logic for state transitions, such as retransmissions and background maintenance tasks. • Schedulers to decide the order of the outgoing packets.
• State machines to maintain per-message state, including the current rate or congestion-window size, sequence and acknowledgement numbers, and message status; and to maintain counters. • Packet generators to support transport protocols that generate control packets in response to data-plane events, such as the arrival of a data packet or the detection of a packet drop.
The transport logic can be implemented with the help of a P4 programmable, event-driven PISA pipeline [22]. Programmability allows us to compare different transport layers, and allows network owners to create new solutions, possibly dynamically deploying workload-specific transport layers. A programmable pipeline means new line-rate packet-processing is easy to add without slowing down application processing in the CPU core, for example network telemetry [32] or new protocol headers.

Purpose-built Contention-free IO for the Network
Recent work has shown that long tail latencies can be caused by bandwidth contention for the main memory; arriving and departing network packet data fights for memory bandwidth with memory accesses by applications [47,48]. Applications usually process network data sequentially; therefore, random access memory is not the right type of resource for networking in the first place. Instead, the nanoPU maintains a dedicated two levels of FIFO queues for network data, allowing independent, sequential, non-contending reads and writes, as shown in Figure 1. On the receive path, the two levels of FIFOs consist of local per-core RX queues and global RX queues shared across cores. 3 On the transmit path, there is a corresponding set of local and global TX queues to store individual message words written by applications.
When an application thread running on a core wishes to perform network IO, it binds to a layer-4 port number. The nanoPU then allocates local and global RX/TX queues for the port. Threads running on the same core must bind to different port numbers, but threads running on different cores are allowed to bind to the same port number, allowing multiple cores to process messages from the same global RX queue.
The per-core FIFOs connect directly to two general purpose registers (GPRs) in the CPU register file; the head (netRX) and tail (netTX) of the network RX and TX queues, respectively. To receive a message, an application simply reads from GPR netRX, pulling data from the head of the network RX queue. Similarly, to send a message, an application writes to the GPR netTX corresponding to the tail of the network TX queue.
The hardware ensures that the correct queues are read from and written to for the current thread, preventing data leakage between separate threads.
While the general architecture could be partitioned and packaged in a number of different ways, we assume here that a nanoPU chip contains the NIC and the cores, and that all buffer and FIFO memory is integrated onto the same chip. Hence, arriving data traverses a dedicated point-to-point link to each core and does not need to wait for a shared PCIe bus.

Hardware Core Selection
Arriving messages must be dispatched to a core for processing by an application thread. If the thread is pinned to a single core, the choice is clear. But more often, applications run threads on many cores, and we want to dispatch an arriving message to an idle core. Ideally, the NIC would maintain a single work-conserving global RX queue from which an idle core can pull its next message to process, leading to the lowest expected waiting time. But this design is impractical, requiring all cores to read from a single global RX queue at the same time. At the other extreme, where the NIC maintains an RX queue for each core, some messages will become stuck in busy cores' RX queues while other cores are sitting idle.
Join-Bounded-Shortest-Queue or JBSQ(n) has been shown to be a good approximation of the ideal single queue system [33,47], and is practical to implement in hardware. JBSQ(n) uses a combination of a centralized queue, plus short bounded queues of maximum depth n for each core. When the per-core queues have available space, the centralized queue will replenish the shortest queue first. JBSQ(1) is equivalent to the single-queue model.
The nanoPU implements the JBSQ(n) policy in hardware, which maps very naturally onto the two levels of RX queues in Figure 1, with one central RX queue for each layer 4 port number (i.e., application). By default, we use JBSQ(2), although the design can be configured to use different values of n. We defer implementation details to Section 3.2.

Hardware Thread Scheduling
The nanoPU thread scheduler has several requirements. First, it must make decisions frequently and quickly. The best stateof-the-art operating systems make scheduling decisions every 5µs [27,41] making them far too coarse-grained to schedule RPCs with sub-microsecond processing times. The nanoPU thread scheduler therefore runs in hardware. This allows scheduling decisions to be made continuously and in parallel with application processing, and without waiting for a timer interrupt to initiate a context switch to a software scheduler.
Second, the thread scheduler must keep track of which threads are currently eligible for scheduling. A thread is marked active and therefore eligible for scheduling if the thread has been registered (which means a port number and RX/TX queues have been allocated) and a message is waiting in the thread's local RX queue. The thread remains active until the thread explicitly indicates that it is idle or its local RX queue is empty.
Third, the thread scheduler must choose which thread to run next. Each thread is allocated a strict priority; a higher priority active thread will preempt a lower priority active thread, while threads of the same priority are scheduled to process messages in FIFO order. As described below, a thread's priority is dynamic: It can be downgraded from priority 0 to priority 1 while running.
Fourth, and unique to the nanoPU, the thread scheduler supports guaranteed service time bounds for conforming applications. The guarantee, which can only be provided to priority 0 threads, works by limiting their message processing time. If a priority 0 thread takes longer than x µs to process a message, the scheduler will immediately downgrade its priority from 0 to 1, allowing it to be preempted by a different priority 0 thread with pending messages. (By default, x = 1µs.) If a core is configured to run at most k priority 0 application threads, and these applications are designed such that only one message per application is outstanding at a given moment, then the message processing tail latency for the applications is bounded by: max(latency) ≤ N + kx + (k − 1)c, where N is the NIC latency, and c is the context-switch latency. Even if some of the applications on the core misbehave and take longer than xµs to process a message, this bound still applies to the others.
Finally, the thread scheduler tells the operating system when to change threads. It does this by firing an interrupt under the following conditions: • The thread currently running on the core is no longer the highest priority active thread. This can happen for a few reasons: (1) a message arrives for a higher priority thread than the one currently running, (2) a high priority thread finishes processing its messages and becomes idle, or (3) a priority 0 thread exceeds its maximum allowed processing time and its priority is lowered to 1. • All threads are idle and the current thread exceeds the idle timeout. In this case, the scheduler rotates through all running threads to ensure that they can all make progress.
To do its job, the thread scheduler takes three pieces of information as inputs: (1) the state of each thread (active or idle, (2) the priority of each thread, and (3) the timestamp of the message at the head of each queue.

Our nanoPU Implementation
We designed a prototype quad-core nanoPU based on the open-source RISC-V Rocket core [46]. A block diagram of our prototype is shown in Figure 2.
Our prototype nanoPU extends the open-source RISC-V Rocket-Chip SOC generator [5], adding 4,300 lines of Chisel [7] code to the code base. The Rocket core is a simple five-stage, in-order, single-issue processor. We use the default Rocket core configuration: 16KB L1 instruction and data caches, a 512KB shared L2 cache, and 16GB of exter-  nal DRAM memory. Everything shown in Figure 2 except the MAC and Serial IO is included in our prototype and it is available as an open-source, reproducible artifact [18]. Our prototype runs a custom-written nanoKernel based on the RISC-V bare-metal boot code, consisting of about 1,200 lines of C code and RISC-V assembly instructions. The nanoKernel is responsible for booting the nanoPU and handling the context switch between threads when instructed by the hardware thread scheduler. As we will discuss further in Section 4, our prototype nanoPU runs on AWS F1 FPGA instances, using the Firesim [31] framework.

Hardware Transport layer
NDP: All our evaluations in this paper are based on the NDP transport protocol [20], which we have implemented on our prototype entirely in hardware. We chose NDP because it has promising low-latency performance, is well-suited to handle small RPC messages (the class of messages we are most interested in accelerating), and it can be readily implemented in our programmable pipeline. See the appendix for a brief summary of NDP functionality.
To evaluate a congested multi-nanoPU system, we added NDP's trimming functionality to Firesim's simulated switches. We leave the evaluation of our programmable transport layer running other congestion control algorithms to a future paper.
Message buffer: At 200Gb/s, a new 64B packet can arrive every 2.5ns. As a result, in order to run at line-rate, message reassembly and packet retransmission must use very simple operations. Three key considerations make it challenging to accomplish this goal: (1) Packets and messages are variable lengths, which makes it difficult to efficiently carve the buffer at high speed; (2) Low-latency transport protocols [20,37] do per-packet load-balancing, so our design must perform message re-sequencing and reassembly; and (3) Network congestion can cause packet drops, so senders must be able to re-transmit packets out of order. The design of our message buffer is simple and not novel, and is described in the appendix.
The key idea is to use fixed size message buffers so that the only operation required to find the position of a packet within a message is to add the appropriate offset to the buffer pointer.
All messages sent and received by applications carry a 64bit application header, which indicates the message length (in bytes), along with the message's source IP address and source port for received messages; and destination IP address and destination port for transmitted messages. The transport logic converts between application message headers and the Ethernet, IP, and NDP headers on each packet.

JBSQ Core Selection in Hardware
As explained above, the nanoPU implements the JBSQ(2) [33] core selection algorithm. JBSQ(2) is implemented using two tables. The first maps the layer 4 port number to a per-core bitmap, indicating whether or not each core is running a thread bound to the port number. The second maps the layer 4 port number to a count of how many messages are outstanding at each core for the given port number. When a new message arrives, the algorithm checks if any of the cores that are running an application thread bound to the destination port are holding fewer than two of the application's messages. If so, it will immediately forward the message to the core with the smallest message count. If all target cores are holding two or more messages for this port number, the algorithm waits until one of the cores indicates that it has finished processing a message for the destination port. It then forwards the next message to that core.

Priority Thread Scheduling in Hardware
The nanoPU implements thread scheduling in hardware, as described in Section 2.4. Our prototype supports running up to four threads on each core; each thread can be configured with a unique priority value. Priority 0 has a configurable maximum message processing time in order to support bounded servicetime applications. We added a new thread-scheduling interrupt to the RISC-V core, along with an accompanying control & status register (CSR) set by the hardware scheduler that tells the nanoKernel trap handler which thread it should run next. When processing very low latency RPCs, we disable all other interrupts to avoid unnecessary traps and context switches.
We define the context-switch latency to be the time from when the scheduler fires the interrupt to when the first instruction of the target thread is executed. Our prototype has a measured context-switch latency of 160 cycles, or 50ns for a 3.2 GHz CPU.

Register File Interface
The RISC-V Rocket core requires surprisingly few changes to support our model of packets entering and leaving via two reserved GPRs. The main change, naturally, involves the register read-write logic. Each core has 32 GPRs, each 64-bits wide, and we have reserved two for network communications. Applications must be compiled to avoid using the reserved GPRs for temporary storage. Fortunately, gcc makes it easy to reserve registers via command-line options [40].
The core also requires changes because of pipeline flushes. A pipeline flush can occur for a number of reasons (e.g., a branch misprediction). On a traditional five-stage RISC-V Rocket core, architectural state is not modified until an instruction reaches the write-back stage (stage 5). However, with the addition of our network register file interface, reading the GPR corresponding to the network RX queue now causes a state modification in the decode stage (stage 2). This destructive read operation must be undone when there is a pipeline flush. In our prototype, the CPU pipeline depth is an upper bound on how many read operations need to be undone; in our case, at most two reads require undoing. Fortunately, it is easy to implement a FIFO queue that supports this operation.

The nanoPU Hardware/Software Interface
To illustrate the mechanics of how software on the nanoPU interacts with the hardware, Listing 1 shows a simple loopbackand-increment program in RISC-V assembly. The program continuously reads 16B messages (two 8B integers) from the network, increments the integers, and sends the messages back to their sender. The program details are described below.
The entry procedure binds the thread to a layer 4 port number at the given priority level by first writing a value to both the lcurport and lcurpriority CSRs, then writing the value 1 to the lniccmd CSR. The lniccmd CSR is a bit vector used by software to send commands to the networking hardware; in this case, it is used to tell the hardware to allocate both local and global RX/TX queues for port 0 with priority 0. The lniccmd CSR can also be used to unbind a port or to update the priority level.
The wait_msg procedure waits for a message to arrive in the local RX queue by polling the lmsgsrdy CSR until it is set by the hardware. While it is waiting, the application tells the hardware thread scheduler that it is idle by writing to the lidle CSR during the polling loop. The scheduler uses the

Listing 1: Loopback with increment. A nanoPU RISC-V assembly program that waits for a 16B message to arrive, increments each word, and returns it to the sender.
idle signal to evict idle threads in order to schedule a new thread that has messages waiting to be processed.
The loopback_plus1_16B procedure simply swaps the source and destination addresses by moving the RX application header (the first word of every received message -see Section 3.1) from the netRX register to the netTX register, shown on line 19 (Listing 1). It then increments every integer in the received message and appends them to the message being transmitted. After the procedure has finished processing the message, it tells the hardware scheduler it is done by writing to the lmsgdone CSR. The scheduler uses this write signal to reset the message processing timer for the thread. It may also evict the thread to ensure that messages arriving for other threads of the same priority are processed in FIFO order. Finally, the procedure waits for the next message to arrive.
Applications that use variable-length messages can use the message length (in the RX application header) to read the correct number of words from the network RX queue. If an application reads an empty RX queue, the resulting behavior is undefined-similar to reading an uninitialized variable.

Evaluation
We evaluate our nanoPU design using micro-benchmarks for latency and throughput, for thread scheduling, load balancing, and congestion control. We run real application benchmarks for the MICA key-value store [34] and the NetChain chainreplication protocol [26].

Methodology
Benchmark tools: For basic latency and throughput microbenchmarks, we use the Verilator [49] cycle-accurate software

Microbenchmarks
a. Wire-to-wire and loopback latency: Figure 2 shows the latency breakdown for a single 8B application message (in a 72B packet) measured from the Ethernet wire through a simple loopback application in the core, then back to the wire. 4 As shown, the loopback latency through the nanoPU is only 13ns, but in practice we also need an Ethernet MAC and serial I/O, leading to a wire-to-wire latency of 65ns. The wire-to-wire latency is about 13× faster than the current state-of-the-art on a commodity server, eRPC [28], which reports a host-stack latency of 850ns.
b. Single core throughput: Table 1 shows the maximum sustainable RX and TX throughput for a single nanoPU core, processing 1KB messages for two applications: one designed to process fixed-length messages and another designed to process variable-length messages. With fixed-length message processing, the send and receive loops can be unrolled, making them three times faster than for variable-length message processing. With loop unrolling, almost all instructions perform network reads and writes, whereas without it, 66% of the instructions are needed to manage the loop (i.e., branch and increment instructions). eRPC [28]   NIC supports a maximum throughput of 350 million requests per second (Mrps), or about 50× higher than existing systems that perform network packet processing and message load balancing in software on a dedicated CPU core [27,41,28].
c. Thread scheduling: We evaluate the performance of nanoPU's hardware thread scheduler (which has its own interrupt) against a more traditional timer-interrupt driven scheduler. In both cases, scheduling decisions are made in hardware. 5 For the timer-interrupt driven thread scheduling policy, we disable the hardware thread scheduler's interrupt and instead configure a timer interrupt to fire every 5µs, at which point the kernel swaps in the highest-priority active thread. We use 5µs timer interrupts to match the granularity of stateof-the-art low latency operating systems [27,41]. We evaluate both schedulers when they are scheduling two threads: one with priority 0 (high) and one with priority 1 (low). We tell the load generator to generate requests with an on-core service time of 500ns (i.e., an ideal system will process 2Mrps). Figure 3 shows the 99% tail latency vs load for both thread scheduling policies, with a high and low priority thread. By allowing the hardware to drive the thread scheduling logic as messages arrive, the tail latency of the high and low priority threads are reduced by 4× and 6.5× at low load, respectively; and it can sustain at least 96% load. 6 d. Bounded message processing time: We evaluate the ability of the nanoPU to bound the tail latency of well-behaved applications, even when they are sharing a core with misbehaving applications. To do this, we configure one of the nanoPU's cores to run two threads, one well-behaved thread and one misbehaving thread. All requests have an on-core service time of 500ns, except when a thread misbehaves (once every 100 requests), in which case the request processing time is 5µs. Both threads are configured to run at priority 0.   Figure 4 shows the 99% tail latency vs load for the wellbehaved and misbehaving threads for the following two experiments: • Bounded time: the bounded message processing time feature of the nanoPU thread scheduler is enabled. If a priority 0 thread takes longer than 1µs to process a request then its priority is lowered to priority 1. • Unbounded time: the bounded message processing time feature of the nanoPU thread scheduler is disabled so both threads remain at the same priority level and all requests are processed by the core in FIFO order.
We expect an application with at most one message at a time in the RX queue, to have a tail latency bounded by 2 · 43ns + 13ns + 2 · 1000ns + 50ns = 2.15µs. This matches our experiments: With the bounded message processing time feature enabled, the tail latency of the well-behaved thread never exceeds 2.1µs, until the offered load on the system exceeds 100% (1.9 Mrps). This is despite using a Poisson arrival process that will occasionally allow more than one message in the RX queue. The nanoPU lowers the priority of the misbehaving application the first time that it takes longer than 1µs to process a request. Hence, the well-behaved application quickly becomes strictly higher priority than the misbehaving application and its requests are never trapped behind a long 5µs request. Note that the bounded message processing time approach can sustain higher loads because, by processing shorter requests first, it keeps the queues smaller. The feature is therefore shown to strictly bound the tail latency for high priority applications.
e. Core selection algorithm: The hardware core selection algorithm steers incoming messages to nanoPU cores for processing. We evaluate and compare three different algorithms using a workload representative of an application like Redis [44]. We assume that 99.5% of messages are simple get/put requests (modeled by a nanoPU service time of 500ns) and 0.5% of messages are complex range queries (with a 5µs service time). We compare three core selection techniques: • RSS (Receive Side Scaling): This is a simple loadbalancing algorithm commonly used by modern NICs. One thread runs on each core and is fed by a separate global RX q q q q q q q q q q q 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% 100% queue (one per-thread, which is also one per-core). Each thread is assigned a unique port number, and the load generator selects a port number uniformly at random. • JBSQ: This is the algorithm described in Section 2.3. We run one thread per core, allocate one global RX queue for all threads (i.e., all threads share the same port number). The JBSQ algorithm load-balances requests to cores. • JBSQ-PRE: In this prioritized version, the short requests are assigned priority 0 (high), and long requests run at priority 1 (low). Each type of request has its own port number. We run two threads on each core (one per-priority) and run JBSQ with strict priority thread scheduling at each core as new messages arrive (described in Section 2.4). Figure 5 shows the 99% tail latency vs load for the three techniques described above. The tail latency of JBSQ is less than RSS because short requests do not get stuck behind long requests, unless all cores are busy processing long requests. In that case, JBSQ-PRE is even better, because the nanoPU thread scheduler will strictly prioritize processing short requests over long requests, preemptively if necessary. JBSQ-PRE sustains higher overall load (almost 100%) because it keeps the queues smaller by processing short requests first.
Our evaluation shows that with the combination of an efficient core selection algorithm and a fast per-message, preemptive, prioritized thread scheduling algorithm, we can sustain very high load and low latency from the nanoPU cores.
f. NDP transport: If the nanoPU and extremely finegrained computing become prevalent, we can expect large amounts of incast, hence our choice of NDP. We therefore evaluate our NDP implementation by running an 80-to-1 incast experiment. The experiment runs on 81 AWS FPGAs simulating 81 nanoPUs with a total of 324 cores; the experiment is coordinated by Firesim. The 81 nanoPUs connect to a single switch via 200 Gb/s links; the RTT of the network is 3µs. All 80 clients send a single 1024B message (in a 1088B packet) to the server at the same time. The bottleneck queue size is 81KB, and is therefore only large enough to hold 74 of the 80 packets; therefore, most of the packets will be queued, while others will be trimmed (when we enable NDP) or dropped (otherwise). We run two experiments, one with NDP congestion control enabled and one with it disabled (by disabling packet trimming in the switch). Figure 6 shows a time series of the occupancy of the bottleneck queue at the switch, with and without NDP enabled. At the beginning, we see all 80 packets arrive at the same time and filling up the switch queue. Without NDP (green line), six packets are silently dropped at the onset of the incast. The senders must infer that their packets were dropped using a timeout. All of the retransmitted packets arrive at the same time, causing a smaller secondary incast. After 13µs the final byte of the final packet arrives.
On the other hand, with NDP enabled, six packets are trimmed and their headers are placed into the control queue and forwarded with high priority. For each TRIM packet received, the server generates a NACK packet and a paced PULL packet to tell the client to retransmit the dropped packet. PULL packets are scheduled so that the retransmitted packets arrive at the bottleneck link at line-rate. In total, it takes 4.2µs for the final byte of the final packet to be serialized onto the bottleneck link, which is about three times quicker than without NDP enabled.

Application Benchmarks
a. Key-value store: We ported the MICA key-value store [34] to run on our quad-core nanoPU prototype. This required minimal changes to the MICA source code; we modified 36 lines of functional code.
To compare with Nebula, our evaluation stores 10k keyvalue pairs (16B keys and 512B values) on each core. Each core holds distinct key ranges, obviating the need for intercore synchronization. The load generator sends a 50:50 mix of read/write queries with keys picked uniformly from the set. Figure 7 compares the 99% tail latency vs load for two different core-selection policies: JBSQ and static core assignment. While using JBSQ core selection for this workload leads to incorrect application behavior (since each request must be serviced by a fixed core dictated by the key), we include it to match how Nebula was evaluated.
It is an ambitious exercise for our tiny Rocket cores to compete with Nebula's much beefier out-of-order, triple-issue, ARM cores running at 2 GHz, with a 16MB LLC, and 45ns DRAM access time. Nonetheless, we see that the nanoPU performs better. At low load, nanoPU running JBSQ leads q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q qq q qqqqqq qq q q q q q q q q to a 592ns 99% tail latency for MICA, and 823ns for static core assignment -including the link latency between the load generator and the nanoPU. Nebula reports a "sub-2µs 99% tail latency." Nebula is able to achieve a maximum per-core load of about 1.5 Mrps, whereas the nanoPU is higher at 2.1 Mrps.
The main takeaway is that the nanoPU, like Nebula, eliminates memory-bandwidth interference and therefore achieves similar low tail-latency and high throughput, but with a less powerful core. The systems use different methods to eliminate memory-bandwidth interference. Nebula limits the amount of LLC space allocated to a particular application based on its average service time, which breaks down when the request processing time is unknown or highly variable. On the other hand, the nanoPU uses a separate hardware FIFOs for network data and is unaffected by variations in request processing time.
b. Chain replication: Strongly consistent, fault-tolerant key-value stores often use chain replication. We evaluate the chain replication protocol from NetChain [26] by porting it to run on multiple nanoPUs, running on top of our MICA key-value store. To WRITE to the store, the client sends a request to the first replica in the chain. The replica applies the WRITE and forwards the request to the next replica in the chain, as indicated in the packet. The last replica sends an ACK to the client to complete the WRITE. READ requests are sent directly to the last node in the chain, which replies directly to the client.
To compare to the NetChain evaluation, we used 10k 16B/64B key/value pairs, and measure the end-to-end latency for a 3-way replicated WRITE from the client through the servers and back. We configure the switch that is connecting all four hosts to forward packets with zero latency and use 43ns link latencies. With three replicas, NetChain reports 9.7µs mean zero-load latency. We measured a mean latency of 1.1µs and a 99% tail latency of just 1.5µs on the nanoPU. The nanoPU client took only 130ns, compared to approximately 3-4µs for the DPDK client used in the NetChain evaluation. NetChain is implemented using programmable switches, hence the deployment is limited in terms of key-value size and capacity constraints, lack of congestion-control/reliable transport, and it relies on an external failure detector. Our implementation does not suffer from these same limitations because the nanoPU is a general-purpose processor, which is more flexible than a programmable switch.

Discussion
The nanoPU is deliberately simple: We believe that minimizing latency requires us to strip away complexity, and move what we can to pipelined hardware. Our prototype is also simple because of the constraints of a university research project. The designer of a complete nanoPU will need to consider additional key tradeoffs.
GPRs vs CSRs. The nanoPU prototype repurposes two GPRs for the head (netRX) and tail (netTX) of the network queues. In a CPU with 32 registers, we can likely afford to lose two; our evaluated applications did not appear to suffer. However, we also experimented with a design where the head and tail registers are implemented using control status registers (CSRs) instead, an alternative to consider if GPRs are limited.
In-order execution. The nanoPU prototype is based on a simple 5-stage, in-order RISC-V Rocket core. While our prototype required very minor modifications to the CPU pipeline, if an out-of-order processor is used, more invasive changes would be required to ensure that words read from the RX queue are in FIFO order.
Floating-point operations. Our prototype places messages directly into the integer register file. Scientific computing applications use floating point arithmetic, and floating point GPRs. The RISC-V ISA [45] includes instructions to copy bits directly between integer and floating point registers, although we have not used them.

Related Work
Low-latency software systems for RPC. Recent work on low-latency RPC systems focuses on load balancing requests across cores by approximating a single-queue system using work-stealing (like ZygOS [43]) or preempting requests at microsecond timescales (Shinjuku [27]) to avoid head-of-line blocking and to manage requests with highly-variable service times. However, the inter-core synchronization and software preemption incurs non-negligible overheads and degrade performance. Software thread scheduling and core selection are too slow and coarse-grained for sub-microsecond RPCs. Therefore, the nanoPU implements them both in hardware leading to significantly higher performance.
Unlike the nanoPU, eRPC [28] takes the other extreme and runs everything in software, and through clever optimizations, achieves impressively low latency on a commodity server for the common case. eRPC has good average performance, but its common-case optimizations sacrifice tail latency, which often dictate application performance. The nanoPU's hardware pipeline and direct coupling to the register file makes median and tail RPC latencies almost identical.
Hardware extensions for RPC systems. Others have proposed implementing core selection algorithms in hardware. RPCvalet [12] and Nebula [47] are both built on top of the Scale-out NUMA architecture [39]. RPCvalet implements a single queue system, which in theory provides optimal performance. However, it ran into memory bandwidth contention issues, which they later resolve in Nebula. Both Nebula and R2P2 [33] implement the JBSQ load balancing policy; Nebula runs JBSQ on the server whereas R2P2 runs JBSQ in a programmable switch. Like Nebula, the nanoPU also implements JBSQ to steer requests to cores.
RDMA is designed to give direct access to a remote server's memory. Many NICs now offer RDMA in hardware and can respond in a few microseconds. Several systems such as HERD [29], FaSST [30], and DrTM+R [11] exploit RDMA to build applications and services on top. However, the nanoPU targets RPC-based applications that need low latency access to remote CPUs, not remote memory.
SmartNICs (NICs with CPUs on them) are in deployment today by cloud service providers (CSPs) [6,36,38], to offload infrastructure software from the main server to CPUs on the NIC. However, these may actually increase the RPC latency, unless they adopt nanoPU-like designs on the NIC.
Transport protocols in hardware. We are not the first to implement the transport layer and congestion control in hardware. Modern NICs that support RDMA over Converged Ethernet (RoCE) already implement DCQN [51] in hardware. In the academic research community, Tonic [3] proposes a framework for implementing congestion control in hardware. The nanoPU's NDP implementation draws upon ideas in Tonic, and goes further to build and evaluate a full system.
Register file interface. GPRs were first used by the Jmachine [13] for low-latency inter-core communication on the same machine, but were abandoned because of the difficulty in isolating threads running on the same core. Moore's Law has helped: From 10 6 transistors per chip in 1989 to over 10 10 today, nanoPU can afford per-thread local queues, not feasible at the time of the J-machine.

Conclusion
Today's RISC CPUs are optimized for load-store operations to and from memory. Memory data is treated as a first-class citizen. But modern workloads frequently process huge numbers of packets, e.g., RPCs for distributed applications and stream processing for NFV. Rather than burden packets with traversing a hierarchy optimized for data sitting in memory, we propose providing them with a new optimized fast path, directly into the heart of the CPU. Hence, we aim to elevate packet data to the same importance as memory data.
We set out to accelerate distributed applications by minimizing RPC tail latency. As applications employ more parallelism, the RPC fanout increases, and so response time is increasingly determined by tail, not median, latency. The bottom line is that, by placing essential functions into hardware, we have driven the RPC tail-latency much closer to the median, potentially accelerating large distributed applications by an order of magnitude.