<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://vishakha-ramani.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://vishakha-ramani.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-10-07T22:42:29+00:00</updated><id>https://vishakha-ramani.github.io/feed.xml</id><title type="html">blank</title><subtitle>Vishakha Ramani, Research Staff Member at IBM T. J. Watson Research Center. Performance modeling and analysis of computing systems, with a focus on LLM inference serving at scale. </subtitle><entry><title type="html">An LLM server in three numbers</title><link href="https://vishakha-ramani.github.io/blog/2026/an-llm-server-in-three-numbers/" rel="alternate" type="text/html" title="An LLM server in three numbers"/><published>2026-07-18T09:00:00+00:00</published><updated>2026-07-18T09:00:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/an-llm-server-in-three-numbers</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/an-llm-server-in-three-numbers/"><![CDATA[<p>Every operator of an LLM inference service faces the same pair of questions. If traffic doubles in the next ten minutes, how many replicas do I need to keep the time to first token under my target? And can I answer that before the queue has already formed? Benchmarking answers the first question one configuration at a time, expensively. A model answers it for configurations nobody has run yet, but only if the model is faithful to how inference servers actually behave.</p> <p>In a <a href="https://arxiv.org/abs/2609.20957">paper written with Asser Tantawi</a>, now available on arXiv, we built that model. The claim that makes it useful is economy. An LLM server, for our purposes, is characterized by three parameters, each with units of time and each with a physical meaning. The first is the baseline overhead of one batched iteration, the kernel launches and synchronization and weight loading that happen regardless of what is in the batch. The second is the compute time per token in a forward pass, set by the accelerator’s arithmetic throughput. The third is the memory access time per token of KV cache, set by its memory bandwidth. Everything the paper predicts is built from these three numbers, the workload’s token lengths, and the arrival rate.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 270" role="img" aria-label="One batched iteration decomposed into baseline overhead plus per-request compute and memory work" xmlns="http://www.w3.org/2000/svg"> <text x="60" y="52" font-size="12" style="fill: var(--fig-muted)">one iteration of the batch</text> <rect x="60" y="64" width="106" height="36" rx="4" style="fill: var(--fig-muted)" opacity="0.35"/> <text x="113" y="87" text-anchor="middle" font-size="13" font-weight="600" style="fill: var(--fig-line)">&#945;</text> <rect x="170" y="64" width="72" height="36" style="fill: var(--fig-rcu)"/> <rect x="244" y="64" width="46" height="36" style="fill: var(--fig-rwl)"/> <rect x="294" y="64" width="72" height="36" style="fill: var(--fig-rcu)"/> <rect x="368" y="64" width="46" height="36" style="fill: var(--fig-rwl)"/> <rect x="418" y="64" width="72" height="36" style="fill: var(--fig-rcu)"/> <rect x="492" y="64" width="46" height="36" style="fill: var(--fig-rwl)"/> <rect x="542" y="64" width="58" height="36" rx="4" stroke-dasharray="4 3" style="fill: none; stroke: var(--fig-muted); stroke-width: 1.25"/> <text x="571" y="87" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">&#8230;</text> <text x="230" y="118" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">request 1</text> <text x="354" y="118" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">request 2</text> <text x="478" y="118" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">request 3</text> <rect x="70" y="146" width="16" height="10" rx="2" style="fill: var(--fig-muted)" opacity="0.35"/> <text x="94" y="155" font-size="12" style="fill: var(--fig-line)">&#945;, the iteration's own overhead, kernel launches and weight loading</text> <rect x="70" y="172" width="16" height="10" rx="2" style="fill: var(--fig-rcu)"/> <text x="94" y="181" font-size="12" style="fill: var(--fig-line)">&#946;, compute time per token, set by arithmetic throughput</text> <rect x="70" y="198" width="16" height="10" rx="2" style="fill: var(--fig-rwl)"/> <text x="94" y="207" font-size="12" style="fill: var(--fig-line)">&#947;, memory time per token of KV cache, set by memory bandwidth</text> <text x="330" y="246" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">with N requests in the batch, the iteration takes &#945; + N&#948;</text> </svg> <figcaption>The anatomy of one batched iteration. A fixed overhead &#945;, then per-token compute and KV-cache work from every request sharing the batch, which is why iteration time is linear in the number of active requests.</figcaption> </figure> <p>The modeling difficulty is that a serving engine like vLLM does not process requests one at a time. It runs continuous batching, where on every iteration boundary a finishing request departs and a waiting one is admitted, so a single forward pass mixes prefill chunks of one request with decode steps of many others. Our way through is a work-centric view. Averaged over its lifetime in the server, a request is equally likely to be found in any of its processing stages, so each active request contributes a calculable average amount of work per iteration. Iteration time then grows linearly in the number of active requests, with slope given by that per-request work and intercept given by the baseline overhead. That linearity is not an assumption of convenience. It shows up plainly in measurements, ours and others’.</p> <p>On top of the work model sits a birth-death Markov chain over the number of requests in the system, with the batch size capped and the arithmetic tracking how the token budget forces long prompts to be split into chunks. Working through the chain yields the two latencies users actually experience, the time to first token and the gap between subsequent tokens, as functions of load. The same arithmetic yields the stability boundary, the arrival rate beyond which the queue grows without bound.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 230" role="img" aria-label="Birth-death chain over the number of requests, with departures speeding up until the batch cap" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="m1ch-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <circle cx="80" cy="110" r="20" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="80" y="115" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">0</text> <circle cx="190" cy="110" r="20" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="190" y="115" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">1</text> <circle cx="300" cy="110" r="20" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="300" y="115" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">2</text> <text x="370" y="115" text-anchor="middle" font-size="13" style="fill: var(--fig-muted)">&#8230;</text> <circle cx="440" cy="110" r="20" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="440" y="115" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">B</text> <circle cx="550" cy="110" r="20" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="550" y="115" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">B+1</text> <path d="M96 96 Q135 74 174 96" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="135" y="72" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">&#955;</text> <path d="M206 96 Q245 74 284 96" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="245" y="72" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">&#955;</text> <path d="M316 96 Q351 76 356 92" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4"/> <path d="M384 92 Q391 78 424 96" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <path d="M456 96 Q495 74 534 96" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="495" y="72" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">&#955;</text> <path d="M174 124 Q135 146 96 124" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="135" y="160" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">&#956;(1)</text> <path d="M284 124 Q245 146 206 124" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="245" y="160" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">2&#956;(2)</text> <path d="M424 124 Q391 142 384 128" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4"/> <path d="M356 128 Q349 144 316 124" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <path d="M534 124 Q495 146 456 124" fill="none" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#m1ch-a)"/> <text x="495" y="160" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">B&#956;(B)</text> <text x="320" y="200" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">departures speed up with the batch until the cap at B, and the service rate</text> <text x="320" y="216" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">in every state comes from the work model above</text> </svg> <figcaption>The birth-death chain over the number of requests in the system. Arrivals push the state up at rate &#955;, departures pull it down faster as the batch grows, and the cap at B is where queueing begins.</figcaption> </figure> <p>A model this compressed has to earn trust in two ways. The first is against data. On a vLLM server with an H100 GPU, across a grid of input and output lengths from 64 to 4096 tokens and a range of arrival rates, the model’s inter-token latency errors are about 5 percent for Llama-3.1-8B and 8 percent for Qwen2.5-14B, with time-to-first-token errors of 14 and 16 percent. The second is against physics. The fitted compute parameter puts the forward pass at roughly 80 percent of the H100’s peak arithmetic throughput, and the fitted memory parameter puts the attention kernel at about 71 percent of peak memory bandwidth. Between the two models, the fitted compute cost scales almost exactly with parameter count. The three numbers are not just degrees of freedom that happen to fit. They come out where physically meaningful quantities should, which is a reason to believe they mean what they claim.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 320" role="img" aria-label="Predicted latency tracks measured latency along the diagonal for both models" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="m1ft-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="262" x2="612" y2="262" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#m1ft-m)"/> <line x1="70" y1="262" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#m1ft-m)"/> <text x="340" y="292" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">measured latency</text> <text x="46" y="150" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 150)">predicted latency</text> <line x1="80" y1="252" x2="560" y2="70" stroke-dasharray="6 4" style="stroke: var(--fig-muted); stroke-width: 1.2"/> <text x="577" y="66" font-size="11.5" style="fill: var(--fig-muted)">y = x</text> <circle cx="96" cy="52" r="4.5" style="fill: var(--fig-rcu)"/> <text x="108" y="56" font-size="12" style="fill: var(--fig-line)">Llama-3.1-8B</text> <path d="M218 47 L223 56 L213 56 z" style="fill: var(--fig-rwl)"/> <text x="230" y="56" font-size="12" style="fill: var(--fig-line)">Qwen2.5-14B</text> <circle cx="104" cy="247" r="4" style="fill: var(--fig-rcu)"/> <circle cx="114" cy="241" r="4" style="fill: var(--fig-rcu)"/> <circle cx="128" cy="231" r="4" style="fill: var(--fig-rcu)"/> <circle cx="138" cy="228" r="4" style="fill: var(--fig-rcu)"/> <circle cx="152" cy="230" r="4" style="fill: var(--fig-rcu)"/> <circle cx="176" cy="212" r="4" style="fill: var(--fig-rcu)"/> <circle cx="205" cy="208" r="4" style="fill: var(--fig-rcu)"/> <circle cx="234" cy="189" r="4" style="fill: var(--fig-rcu)"/> <circle cx="262" cy="187" r="4" style="fill: var(--fig-rcu)"/> <circle cx="296" cy="167" r="4" style="fill: var(--fig-rcu)"/> <circle cx="330" cy="162" r="4" style="fill: var(--fig-rcu)"/> <circle cx="368" cy="139" r="4" style="fill: var(--fig-rcu)"/> <path d="M138 231 L143 240 L133 240 z" style="fill: var(--fig-rwl)"/> <path d="M176 204 L181 213 L171 213 z" style="fill: var(--fig-rwl)"/> <path d="M224 200 L229 209 L219 209 z" style="fill: var(--fig-rwl)"/> <path d="M272 166 L277 175 L267 175 z" style="fill: var(--fig-rwl)"/> <path d="M320 165 L325 174 L315 174 z" style="fill: var(--fig-rwl)"/> <path d="M358 131 L363 140 L353 140 z" style="fill: var(--fig-rwl)"/> <path d="M397 139 L402 148 L392 148 z" style="fill: var(--fig-rwl)"/> <path d="M435 98 L440 107 L430 107 z" style="fill: var(--fig-rwl)"/> <path d="M474 114 L479 123 L469 123 z" style="fill: var(--fig-rwl)"/> <path d="M512 65 L517 74 L507 74 z" style="fill: var(--fig-rwl)"/> </svg> <figcaption>An illustrative version of the validation scatter. Predicted latency tracks measured latency along the diagonal across two orders of magnitude, with the largest deviations at the heaviest Qwen2.5-14B operating points.</figcaption> </figure> <p>The part of this work I am most attached to is that the model tunes itself. Nobody wants to rerun a calibration suite for every model and accelerator pair, and operating conditions drift as batch composition and cache pressure change. So the parameters are estimated from the latencies the service is already observing, first by an initial fit over a handful of observations, then refreshed continuously over a sliding window at every control cycle. A model that maintains its own calibration can live inside a controller indefinitely.</p> <p>And that is where we put it. We embedded the model and its tuner in an autoscaling control loop and ran it against a real vLLM deployment of Llama-3.1-8B on H100 GPUs in an OpenShift cluster, with a 50 millisecond target on time to first token. Under a trapezoidal load ramp from 120 to 480 requests per minute, the controller scaled from one replica to a peak of four, settled at three under sustained peak, and scaled back symmetrically as load fell. The visible latency spikes during scale-up come from the roughly 90 seconds a new pod needs to become ready, a hardware and orchestration effect the model deliberately does not claim to predict. What the model does predict, the steady-state operating point, tracked observed latency within a few milliseconds through the baseline phases.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 430" role="img" aria-label="Autoscaling trace with observed and predicted time to first token above, replicas and load below" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="m1tr-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <rect x="80" y="28" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="104" y="34" font-size="12" style="fill: var(--fig-line)">observed TTFT</text> <rect x="216" y="28" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="240" y="34" font-size="12" style="fill: var(--fig-line)">model prediction</text> <line x1="360" y1="30" x2="378" y2="30" stroke-dasharray="5 3" style="stroke: var(--fig-line); stroke-width: 1.5"/> <text x="386" y="34" font-size="12" style="fill: var(--fig-line)">50 ms target</text> <line x1="70" y1="190" x2="600" y2="190" style="stroke: var(--fig-muted); stroke-width: 1"/> <text x="46" y="120" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)" transform="rotate(-90 46 120)">time to first token</text> <line x1="70" y1="150" x2="600" y2="150" stroke-dasharray="5 3" style="stroke: var(--fig-line); stroke-width: 1.2"/> <text x="150" y="66" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">new pod loading weights,</text> <text x="150" y="81" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">about 90 seconds</text> <line x1="196" y1="72" x2="224" y2="62" style="stroke: var(--fig-muted); stroke-width: 0.8"/> <path d="M75 160 L110 156 L140 161 L170 158 L200 157 L220 150 L230 55 L240 80 L250 152 L280 156 L310 150 L325 90 L335 120 L345 153 L380 150 L420 155 L450 152 L490 157 L530 155 L570 159" fill="none" style="stroke: var(--fig-rcu); stroke-width: 1.8"/> <path d="M75 163 C200 161 400 158 570 160" fill="none" style="stroke: var(--fig-rwl); stroke-width: 1.8"/> <path d="M75 360 L160 360 L200 320 L290 320 L320 285 L420 285 L450 320 L510 320 L540 360 L575 360" fill="none" stroke-dasharray="5 4" style="stroke: var(--fig-muted); stroke-width: 1.5"/> <path d="M75 365 L190 365 L190 340 L250 340 L250 315 L330 315 L330 290 L380 290 L380 315 L460 315 L460 340 L520 340 L520 365 L575 365" fill="none" style="stroke: var(--fig-line); stroke-width: 2"/> <text x="112" y="354" font-size="11.5" font-weight="600" style="fill: var(--fig-line)">replicas</text> <text x="370" y="273" font-size="11.5" style="fill: var(--fig-muted)">arrival rate</text> <line x1="70" y1="395" x2="600" y2="395" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#m1tr-m)"/> <text x="335" y="420" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">elapsed time, about 38 minutes</text> </svg> <figcaption>An illustrative version of the autoscaling trace. Observed time to first token stays near the 50 ms target except during pod startup, while the replica count follows the trapezoidal load.</figcaption> </figure> <div class="key-result">Three time parameters, one Markov chain, and a self-refreshing fit are enough to predict serving latency within a few percent and to hold a 50 ms target on real GPUs.</div> <p>This work now ships as the <a href="https://github.com/llm-d/llm-d-workload-variant-autoscaler/tree/main/internal/engines/analyzers/queueingmodel">queueing model analyzer and tuner</a> inside the Workload Variant Autoscaler, the component of the open source <a href="https://github.com/llm-d/llm-d">llm-d</a> project that scales inference deployments against their service level objectives. The larger argument, the one that runs through everything I work on, is that serving AI needs models with structure, not just measurements with trend lines. This paper is that argument in its smallest complete form.</p> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="llm-serving"/><category term="queueing"/><category term="performance-modeling"/><category term="autoscaling"/><summary type="html"><![CDATA[A note on recent work with Asser Tantawi. A queueing model of LLM inference with three hardware parameters, accurate enough to drive an autoscaler and simple enough to tune itself.]]></summary></entry><entry><title type="html">When a pipeline should stop working</title><link href="https://vishakha-ramani.github.io/blog/2026/when-a-pipeline-should-stop-working/" rel="alternate" type="text/html" title="When a pipeline should stop working"/><published>2026-07-18T00:03:00+00:00</published><updated>2026-07-18T00:03:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/when-a-pipeline-should-stop-working</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/when-a-pipeline-should-stop-working/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Reading and computing on time · Part 3 of 3</span><br/> <a href="/blog/2026/sometimes-fresher-means-waiting/">Sometimes fresher means waiting</a> · <a href="/blog/2026/the-best-time-to-read-memory/">The best time to read memory</a> · When a pipeline should stop working </div> <p>An object detector does not produce answers in one motion. A camera frame is pre-processed, features are extracted, and only then does classification yield something a decision can act on. Augmented reality and smart-city applications offload exactly this kind of work to nearby edge nodes, and the result is only useful if it is still current when it arrives. Multi-step processing is everywhere in real-time systems, yet the freshness literature had largely treated computation as a single act.</p> <p>In <code class="language-plaintext highlighter-rouge">Timely and Energy-Efficient Multi-Step Update Processing</code>, written with Ivan Seskar and Roy Yates, we studied two-step processing as the fundamental building block. Two servers can be arranged in series, each owning one step of a pipeline, or in parallel, each running both steps independently. The judge of every arrangement is the age of information at the monitor receiving the finished updates. The constraint that makes the problem interesting is power.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 350" role="img" aria-label="Two-step processing with servers in series or in parallel" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2ar-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <rect x="200" y="18" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="224" y="24" font-size="12" style="fill: var(--fig-line)">step 1</text> <rect x="290" y="18" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="314" y="24" font-size="12" style="fill: var(--fig-line)">step 2</text> <text x="60" y="70" font-size="13" font-weight="600" style="fill: var(--fig-line)">Servers in series</text> <rect x="120" y="90" width="130" height="48" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <rect x="120" y="90" width="130" height="6" rx="3" style="fill: var(--fig-rcu)"/> <text x="185" y="120" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Server 1, step 1</text> <line x1="250" y1="114" x2="296" y2="114" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2ar-a)"/> <rect x="300" y="90" width="130" height="48" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <rect x="300" y="90" width="130" height="6" rx="3" style="fill: var(--fig-rwl)"/> <text x="365" y="120" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Server 2, step 2</text> <line x1="430" y1="114" x2="486" y2="114" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2ar-a)"/> <circle cx="522" cy="114" r="26" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="522" y="119" text-anchor="middle" font-size="11.5" style="fill: var(--fig-line)">Monitor</text> <text x="275" y="162" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">each server owns one step, updates age while queued between them</text> <text x="60" y="208" font-size="13" font-weight="600" style="fill: var(--fig-line)">Servers in parallel</text> <rect x="120" y="228" width="200" height="44" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <rect x="120" y="228" width="100" height="6" rx="3" style="fill: var(--fig-rcu)"/> <rect x="220" y="228" width="100" height="6" rx="3" style="fill: var(--fig-rwl)"/> <text x="220" y="255" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Server 1, steps 1 and 2</text> <rect x="120" y="286" width="200" height="44" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <rect x="120" y="286" width="100" height="6" rx="3" style="fill: var(--fig-rcu)"/> <rect x="220" y="286" width="100" height="6" rx="3" style="fill: var(--fig-rwl)"/> <text x="220" y="313" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Server 2, steps 1 and 2</text> <line x1="320" y1="250" x2="482" y2="272" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2ar-a)"/> <line x1="320" y1="308" x2="482" y2="286" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2ar-a)"/> <circle cx="522" cy="279" r="26" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="522" y="284" text-anchor="middle" font-size="11.5" style="fill: var(--fig-line)">Monitor</text> <text x="522" y="322" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">keeps the freshest</text> <text x="522" y="336" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">update it receives</text> </svg> <figcaption>The two arrangements. In series, each server owns one processing step. In parallel, each server runs both steps and the monitor keeps whichever finished update is freshest.</figcaption> </figure> <p>The power model is where the numbers get vivid. Dynamic power in CMOS processors follows an alpha-power law, and with the technology exponent we use, power grows as the fifth power of processing speed. Running a step twice as fast costs roughly thirty two times the power. At that exchange rate, no one can afford to buy freshness with raw speed alone. The budget has to be spent where it changes what the monitor knows.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="Power grows as the fifth power of processing speed" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2pw-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="80" y1="242" x2="612" y2="242" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2pw-m)"/> <line x1="80" y1="242" x2="80" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2pw-m)"/> <text x="340" y="272" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">processing speed</text> <text x="56" y="140" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 56 140)">power drawn</text> <path d="M100 238 C260 236 380 228 450 190 C510 156 545 100 565 52" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <circle cx="300" cy="233" r="4.5" style="fill: var(--fig-line)"/> <line x1="300" y1="233" x2="300" y2="242" stroke-dasharray="2 3" style="stroke: var(--fig-muted); stroke-width: 0.8"/> <text x="300" y="215" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">1&#215; speed, 1&#215; power</text> <circle cx="551" cy="82" r="4.5" style="fill: var(--fig-line)"/> <line x1="551" y1="82" x2="551" y2="242" stroke-dasharray="2 3" style="stroke: var(--fig-muted); stroke-width: 0.8"/> <text x="455" y="66" text-anchor="middle" font-size="12" font-weight="600" style="fill: var(--fig-line)">2&#215; speed, 32&#215; power</text> <text x="230" y="120" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">at this exchange rate, freshness</text> <text x="230" y="136" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">cannot be bought with speed alone</text> </svg> <figcaption>The alpha-power law with exponent five. Doubling a step&rsquo;s speed multiplies its power draw by about thirty two.</figcaption> </figure> <p>That framing exposes a phenomenon we call wasted power, which is work that never reduces the age at the monitor. It comes in several flavors. In parallel setups, one server can labor to finish an update while the other has already delivered a fresher one, making the first server’s effort worthless on arrival. In series setups, an update can be preempted or discarded mid-pipeline, taking the energy already spent on it along. Even idleness is a form of waste, capacity that could have been informing the monitor and was not. A pipeline can be fully busy and still be spending watts on information that will land too late to matter.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="A parallel server keeps processing an update the monitor has already outgrown, wasting power" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2ws-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> <marker id="s2ws-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> <pattern id="s2ws-hatch" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse"> <line x1="0" y1="0" x2="0" y2="9" style="stroke: var(--fig-line)" stroke-width="1.4" opacity="0.55"/> </pattern> </defs> <text x="20" y="76" font-size="12" style="fill: var(--fig-muted)">Server A</text> <line x1="90" y1="72" x2="590" y2="72" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <rect x="180" y="58" width="140" height="27" rx="4" style="fill: var(--fig-rcu)"/> <text x="250" y="48" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">processing a newer update</text> <text x="20" y="146" font-size="12" style="fill: var(--fig-muted)">Server B</text> <line x1="90" y1="142" x2="590" y2="142" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <rect x="120" y="128" width="330" height="27" rx="4" style="fill: var(--fig-rwl)"/> <rect x="320" y="128" width="130" height="27" rx="4" style="fill: url(#s2ws-hatch)"/> <text x="205" y="118" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">processing an older update</text> <text x="385" y="176" text-anchor="middle" font-size="11.5" font-weight="600" style="fill: var(--fig-line)">wasted power</text> <line x1="320" y1="85" x2="320" y2="222" style="stroke: var(--fig-line); stroke-width: 1.4" marker-end="url(#s2ws-a)"/> <line x1="450" y1="155" x2="450" y2="222" stroke-dasharray="4 3" style="stroke: var(--fig-muted); stroke-width: 1.2" marker-end="url(#s2ws-m)"/> <text x="20" y="236" font-size="12" style="fill: var(--fig-muted)">Monitor</text> <line x1="90" y1="232" x2="590" y2="232" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#s2ws-m)"/> <circle cx="320" cy="232" r="5" style="fill: var(--fig-line)"/> <text x="320" y="256" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">accepted, age drops</text> <circle cx="450" cy="232" r="5" style="fill: var(--fig-surface); stroke: var(--fig-muted); stroke-width: 1.5"/> <text x="463" y="256" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">older on arrival, discarded</text> </svg> <figcaption>Wasted power in a parallel setup. Once the faster server delivers, everything the slower server does for its older update is spent for nothing. Illustrative.</figcaption> </figure> <p>We built the series side as a family of four queueing disciplines, distinguished by what happens when the first server hands a fresh update to a second server that is still busy. In the fully preemptive design, the newcomer replaces the update in service and the displaced work is thrown away. A second design adds a waiting room holding exactly one update, where a newer arrival replaces whatever waits but never interrupts service. A third blocks instead, discarding fresh arrivals while the second server works. The fourth is synchronous, admitting a new update only when both servers stand idle, so a single update moves through the pipeline at a time. In every one of them the first server generates a fresh update the moment it finishes the previous one, so all the interesting congestion happens at the hand-off.</p> <p>The parallel side is a spectrum of how much the two servers know about each other. At one end, each server runs both steps of its own update independently and delivers whenever it happens to finish. The coordinated alternating policy adds awareness, so both servers start on the same fresh update, and a server that falls behind abandons its work and restarts fresh once its twin carries a newer update into step two. The shared-intermediate policy goes furthest. Whichever server finishes step one first hands the intermediate result to both, they attack step two together, and the pair advances through the pipeline in lockstep. That last policy effectively fuses two processors into a single server that runs each step at twice the rate and never duplicates work.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="The three parallel policies ordered by how much the servers coordinate" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2po-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <g> <text x="110" y="30" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">Independent</text> <rect x="35" y="52" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="35" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="110" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="110" y="75" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server A</text> <rect x="35" y="132" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="35" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="110" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="110" y="155" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server B</text> <text x="110" y="112" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">no signals</text> <text x="110" y="196" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">each works alone,</text> <text x="110" y="211" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">deliveries can arrive already stale</text> </g> <g> <text x="320" y="30" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">Coordinated restarts</text> <rect x="245" y="52" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="245" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="320" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="320" y="75" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server A</text> <rect x="245" y="132" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="245" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="320" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="320" y="155" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server B</text> <line x1="320" y1="92" x2="320" y2="126" stroke-dasharray="4 3" style="stroke: var(--fig-muted); stroke-width: 1.2" marker-start="url(#s2po-m)" marker-end="url(#s2po-m)"/> <text x="330" y="112" font-size="10.5" style="fill: var(--fig-muted)">watches</text> <text x="320" y="196" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">a server behind its twin</text> <text x="320" y="211" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">abandons and restarts fresh</text> </g> <g> <text x="530" y="30" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">Shared intermediate</text> <rect x="455" y="52" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="455" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="530" y="52" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="530" y="75" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server A</text> <rect x="455" y="132" width="150" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="455" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <rect x="530" y="132" width="75" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="530" y="155" text-anchor="middle" font-size="11" style="fill: var(--fig-line)">Server B</text> <line x1="512" y1="92" x2="512" y2="126" style="stroke: var(--fig-line); stroke-width: 1.6"/> <line x1="548" y1="92" x2="548" y2="126" style="stroke: var(--fig-line); stroke-width: 1.6"/> <text x="530" y="196" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">one step one result shared,</text> <text x="530" y="211" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">both attack step two together</text> </g> <line x1="80" y1="252" x2="560" y2="252" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#s2po-m)"/> <text x="320" y="278" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">more coordination, less wasted power</text> </svg> <figcaption>The three parallel policies as a spectrum of coordination. The more each server knows about its twin, the less power it spends on updates that cannot help.</figcaption> </figure> <p>Analytically, the series designs yield to the stochastic hybrid systems machinery that runs through the rest of my dissertation. The parallel designs forced an extension. When servers deliver independently, an arriving update only matters if it is fresher than what the monitor already holds, so the monitor’s age is a running minimum over competing deliveries. Standard age bookkeeping cannot express that, and the fix is to carry auxiliary state variables that track minima of ages across servers, a device we adapted from the age-of-gossip literature. Getting those reset maps right is the technical heart of the paper’s parallel results.</p> <p>Then comes the optimization. Fix a power budget and ask how fast each step should run. Two structural facts make the problem tractable. In every model we analyzed, the average age at the monitor comes out as a function of the rate ratio \(\rho = \mu_1/\mu_2\) divided by the step two rate \(\mu_2\). And the power constraint caps \(\mu_2\) through a quantity we called the power-weighted processor activity, which also depends only on \(\rho\). A two-variable design problem therefore collapses to a one-dimensional search over the ratio. For the preemptive series design, the age is \(\tfrac{1}{\mu_2}(1 + \tfrac{1}{\rho})\), and pushing the budget through the constraint puts the optimum at \(\rho^* \approx 0.846\).</p> <p>Two findings hold across every model we studied. The second step should always run at least as fast as the first, and no model wants the ratio above one. A slow exit stage is a bottleneck where nearly finished updates sit and age, which is the worst place to save power. And the optimal ratio does not depend on the size of the budget. The right split between the steps is structural, so extra power should scale both steps together rather than shift the balance.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 320" role="img" aria-label="Average age against the rate ratio has its minimum at the same ratio for every power budget" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2rt-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="252" x2="612" y2="252" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2rt-m)"/> <line x1="70" y1="252" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2rt-m)"/> <text x="340" y="296" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">rate ratio &#961; = &#956;&#8321;/&#956;&#8322;</text> <text x="46" y="145" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 145)">average age at the monitor</text> <line x1="360" y1="252" x2="360" y2="259" style="stroke: var(--fig-muted); stroke-width: 1"/> <text x="360" y="273" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">&#961; = 1</text> <line x1="291" y1="252" x2="291" y2="88" stroke-dasharray="6 4" style="stroke: var(--fig-line); stroke-width: 1.3"/> <text x="291" y="60" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">the optimum, &#961;* &#8776; 0.846</text> <text x="291" y="76" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">step two faster than step one</text> <rect x="440" y="50" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="464" y="56" font-size="12" style="fill: var(--fig-line)">smaller budget</text> <rect x="440" y="70" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="464" y="76" font-size="12" style="fill: var(--fig-line)">larger budget</text> <path d="M95 118 C180 168 250 190 291 192 C340 194 460 172 585 118" fill="none" style="stroke: var(--fig-rwl); stroke-width: 2"/> <path d="M95 172 C180 216 250 232 291 234 C340 236 460 216 585 172" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <circle cx="291" cy="192" r="4.5" style="fill: var(--fig-rwl)"/> <circle cx="291" cy="234" r="4.5" style="fill: var(--fig-rcu)"/> </svg> <figcaption>An illustrative version of the optimization result. More power lowers the whole curve, but the age-minimizing split between the steps stays at the same ratio, with step two always faster.</figcaption> </figure> <p>The ranking of designs held surprises too. Among series disciplines, preemption in service wins, consistent with the freshness folklore that new should replace old, and the gap is not subtle. At the same service rates, the blocking design delivers exactly twice the average age of the preemptive one. Less expected, synchronous service beats the asynchronous designs, meaning one update moving through the system at a time outperforms designs that keep more in flight only to discard or delay them. Parallelism helps on top of that. Running the synchronous discipline on two independent servers, each on half the power budget, already beats its series counterpart, and the best design overall is the shared-intermediate policy. The common thread is that coordination is what eliminates wasted power. Servers that know what their peers have delivered do not burn watts on obsolete work.</p> <div class="key-result">At power that grows like the fifth power of speed, coordination beats raw speed. The best designs are the ones that never spend watts on updates the monitor has already outgrown.</div> <p>One caveat belongs in the record. These models use exponential processing times, where preempting a job loses no accumulated progress in expectation. With service times whose completion becomes more certain as work accumulates, killing a nearly-finished update is genuinely wasteful, and the clean definition of wasted power blurs. Extending the framework there is open work.</p> <p>This paper closes the arc of the series, and of this half of my dissertation. Wait before computing when variance is high. Read at the moment the threshold says to. And stop working when the work cannot make anyone better informed. I now spend my days on inference systems that run a two-phase workload, a compute-heavy prefill followed by a memory-bound decode, under latency targets and power budgets. The vocabulary has changed from monitors and updates to tokens and service level objectives, but the accounting question underneath is the one this paper asked. Which watts are actually buying information?</p> <div class="series-nav-footer"> <span> &larr; <a href="/blog/2026/the-best-time-to-read-memory/">The best time to read memory</a> </span> <span> </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="queueing"/><category term="performance-modeling"/><category term="energy"/><summary type="html"><![CDATA[Part three of a second series on my PhD work. In multi-step update processing, busy servers can waste power on results that make no one better informed.]]></summary></entry><entry><title type="html">The best time to read memory</title><link href="https://vishakha-ramani.github.io/blog/2026/the-best-time-to-read-memory/" rel="alternate" type="text/html" title="The best time to read memory"/><published>2026-07-18T00:02:00+00:00</published><updated>2026-07-18T00:02:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/the-best-time-to-read-memory</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/the-best-time-to-read-memory/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Reading and computing on time · Part 2 of 3</span><br/> <a href="/blog/2026/sometimes-fresher-means-waiting/">Sometimes fresher means waiting</a> · The best time to read memory · <a href="/blog/2026/when-a-pipeline-should-stop-working/">When a pipeline should stop working</a> </div> <p>How often should a reader check shared memory for fresh data? Checking is not free. A read can cost bandwidth, processor time, or the latency of a query to a distant database. And a read that arrives before anything has changed buys nothing at all. The system pays full price to learn what it already knew, while a reader that checks too rarely lets its client drift out of date. Everyone who has refreshed a page that has not changed knows both failure modes personally.</p> <p>In <code class="language-plaintext highlighter-rouge">Efficient and Timely Memory Access</code>, written with Ivan Seskar and Roy Yates, we made this trade-off exact. The model is deliberately spare. Time is slotted. In each slot a writer publishes a fresh update to memory with probability \(p\). A reader then chooses to idle or to sample the memory at a fixed cost \(c\), delivering whatever it reads to a client. The state is a pair of ages, the age of the update in memory and the age of the client’s copy, and the goal is to minimize the long-run average of client age plus sampling cost.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 250" role="img" aria-label="A slotted system where a writer publishes with probability p and a reader pays c to sample" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2sl-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> <marker id="s2sl-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <rect x="30" y="60" width="86" height="46" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="73" y="88" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Writer</text> <line x1="116" y1="83" x2="162" y2="83" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2sl-a)"/> <text x="139" y="72" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">publish</text> <rect x="166" y="55" width="110" height="56" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="166" y1="83" x2="276" y2="83" style="stroke: var(--fig-line); stroke-width: 0.75"/> <text x="221" y="45" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">memory, age x</text> <line x1="276" y1="83" x2="322" y2="83" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2sl-a)"/> <text x="299" y="72" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">sample</text> <rect x="326" y="60" width="120" height="46" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="386" y="88" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Reader</text> <text x="386" y="126" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">each slot, idle or pay c to read</text> <line x1="446" y1="83" x2="492" y2="83" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2sl-a)"/> <circle cx="530" cy="83" r="28" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="530" y="88" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Client</text> <text x="530" y="130" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">age y</text> <line x1="60" y1="190" x2="580" y2="190" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#s2sl-m)"/> <line x1="100" y1="185" x2="100" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="160" y1="185" x2="160" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="220" y1="185" x2="220" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="280" y1="185" x2="280" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="340" y1="185" x2="340" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="400" y1="185" x2="400" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="460" y1="185" x2="460" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <line x1="520" y1="185" x2="520" y2="195" style="stroke: var(--fig-muted); stroke-width: 1"/> <path d="M155 172 L165 172 L160 181 z" style="fill: var(--fig-muted)"/> <path d="M215 172 L225 172 L220 181 z" style="fill: var(--fig-muted)"/> <path d="M395 172 L405 172 L400 181 z" style="fill: var(--fig-muted)"/> <path d="M515 172 L525 172 L520 181 z" style="fill: var(--fig-muted)"/> <text x="320" y="220" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">slotted time, a fresh update lands in a slot with probability p</text> </svg> <figcaption>The model. In each slot the writer may publish a fresh update, and the reader chooses between idling and paying c to bring the client up to date.</figcaption> </figure> <p>Posing this as a Markov decision process is natural, but there is a technical catch worth naming. Ages grow without bound, so the state space is countably infinite and the costs are unbounded, and in that regime a well-behaved optimal policy is not guaranteed to exist at all. Part of the paper’s work is verifying, through conditions from average-cost dynamic programming, that one does.</p> <p>The answer has the simplest structure the problem allows. The optimal policy is a threshold. The reader should sample only when the memory holds an update it has not yet delivered and the client’s age has reached a critical value \(Y_0\). Everything else follows from that rule. If the memory refreshes and the reader judges the client still fresh enough to skip it, the right move is to keep idling until the memory changes again, because sampling in between would pay \(c\) to fetch what it already declined.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 330" role="img" aria-label="The optimal reader samples at the first fresh update after the client age crosses the threshold" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2th-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="262" x2="612" y2="262" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2th-m)"/> <line x1="70" y1="262" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2th-m)"/> <text x="340" y="308" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">time in slots</text> <text x="46" y="150" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 150)">age at the client</text> <line x1="70" y1="120" x2="600" y2="120" stroke-dasharray="6 4" style="stroke: var(--fig-line); stroke-width: 1.3"/> <text x="80" y="110" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">threshold Y&#8320;</text> <path d="M85 246 L320 118 L360 96 L360 238 L560 128 L575 120" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <path d="M175 272 L185 272 L180 281 z" style="fill: var(--fig-muted)"/> <path d="M355 272 L365 272 L360 281 z" style="fill: var(--fig-muted)"/> <text x="180" y="296" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">fresh update</text> <text x="360" y="296" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">fresh update</text> <line x1="180" y1="268" x2="180" y2="200" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.8"/> <text x="250" y="234" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">below Y&#8320;, skip and keep idling</text> <line x1="360" y1="268" x2="360" y2="245" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.8"/> <text x="452" y="70" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">first fresh update past the threshold,</text> <text x="452" y="85" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">sample and reset the client</text> <line x1="400" y1="90" x2="364" y2="150" style="stroke: var(--fig-muted); stroke-width: 0.8"/> </svg> <figcaption>The optimal policy in action. The reader idles while the client&rsquo;s age is below the threshold, then samples at the first fresh update past it. Illustrative.</figcaption> </figure> <p>Better still, the threshold has a closed form. The optimal \(Y_0\) is the ceiling of \(\sqrt{2c + (1/p - 1/2)^2} - (1/p - 1/2)\), which grows like the square root of the sampling cost. The paper pairs this with a matching lower bound on the achievable average cost, so the formula is not just a heuristic that happens to work. Plotted against the threshold, the average cost traces the U shape this series keeps meeting. Sample too eagerly and the costs pile up faster than the age falls. Sample too rarely and the client’s age dominates everything.</p> <p>The part I find most counterintuitive is how the threshold moves with the update rate. When updates arrive frequently, the optimal reader becomes choosier, not hungrier. Skipping a fresh update is cheap when an even fresher one is due shortly, so a high \(p\) raises the bar for acting. When updates are rare, the reader should take what it can get. Patience, it turns out, is something you can afford only when the world changes quickly.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="The optimal threshold rises with the update probability and with the sampling cost" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2ch-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="242" x2="612" y2="242" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2ch-m)"/> <line x1="70" y1="242" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2ch-m)"/> <text x="340" y="272" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">how often the memory refreshes</text> <text x="46" y="140" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 140)">optimal threshold Y&#8320;</text> <rect x="90" y="50" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="114" y="56" font-size="12" style="fill: var(--fig-line)">expensive reads</text> <rect x="230" y="50" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="254" y="56" font-size="12" style="fill: var(--fig-line)">cheap reads</text> <path d="M90 205 L170 205 L170 190 L260 190 L260 172 L370 172 L370 152 L480 152 L480 140 L585 140" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <path d="M90 168 L160 168 L160 140 L240 140 L240 112 L340 112 L340 88 L460 88 L460 74 L585 74" fill="none" style="stroke: var(--fig-rwl); stroke-width: 2"/> <text x="470" y="215" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">frequent updates make the reader choosier,</text> <text x="470" y="230" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">a better opportunity is always close</text> </svg> <figcaption>An illustrative version of the paper&rsquo;s threshold plot. The optimal threshold climbs with the update rate and with the sampling cost.</figcaption> </figure> <div class="key-result">The optimal reader is a threshold rule with a closed form, and more frequent updates make it choosier rather than hungrier.</div> <p>This paper sharpened the story of the <a href="/blog/2026/sometimes-fresher-means-waiting/">first post</a> in a specific way. There, the reader was blind to the memory and lazy on a timer, and laziness helped mainly by absorbing variance in computation times. Here the reader can see the age of what is in memory, and with that information plus a price per read, the optimal behavior stops being a vague preference for patience and becomes a rule a system could implement in a few lines. Thresholds are the kind of structure practitioners can actually deploy, and proving one optimal rules out the entire zoo of more complicated policies.</p> <p>One honest limitation points forward. The reader in this model is told when memory changes. A reader without that signal cannot wait for evidence of a fresh update, and the paper shows the age achieved with full knowledge is a lower bound on what such a reader can do, while leaving the optimal blind policy open. And in both papers so far, computing on the data was a single act. Real updates pass through several processing steps before they are useful, each step takes time, and running steps faster costs power at a brutal exchange rate. That is the <a href="/blog/2026/when-a-pipeline-should-stop-working/">final post</a> in this series.</p> <div class="series-nav-footer"> <span> &larr; <a href="/blog/2026/sometimes-fresher-means-waiting/">Sometimes fresher means waiting</a> </span> <span> <a href="/blog/2026/when-a-pipeline-should-stop-working/">When a pipeline should stop working</a> &rarr; </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="memory-access"/><category term="performance-modeling"/><summary type="html"><![CDATA[Part two of a second series on my PhD work. The optimal policy for sampling shared memory turns out to be a threshold with a closed form.]]></summary></entry><entry><title type="html">Sometimes fresher means waiting</title><link href="https://vishakha-ramani.github.io/blog/2026/sometimes-fresher-means-waiting/" rel="alternate" type="text/html" title="Sometimes fresher means waiting"/><published>2026-07-18T00:01:00+00:00</published><updated>2026-07-18T00:01:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/sometimes-fresher-means-waiting</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/sometimes-fresher-means-waiting/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Reading and computing on time · Part 1 of 3</span><br/> Sometimes fresher means waiting · <a href="/blog/2026/the-best-time-to-read-memory/">The best time to read memory</a> · <a href="/blog/2026/when-a-pipeline-should-stop-working/">When a pipeline should stop working</a> </div> <p>Picture the compute node behind a smart intersection. Cameras and roadside sensors publish measurements continuously. An analytics process reads the latest data, spends some time computing a safety decision, and delivers the result to a monitor that acts on it. The monitor does not care how busy the pipeline was. It cares how old the information behind the current decision is by the time that decision arrives.</p> <p>In <code class="language-plaintext highlighter-rouge">Timely Processing of Updates from Multiple Sources</code>, written with Ivan Seskar and Roy Yates, we modeled this loop as a publish-subscribe system. Two independent sources push time-stamped updates through a writer into shared memory, with Read-Copy-Update arbitrating between writer and reader, the same mechanism the <a href="/blog/2026/a-lock-can-make-data-fresher/">first series</a> dissected. A decision process subscribes by reading the latest pair of source updates, computes on it for a random time, and sends the derived decision update onward. Then it faces its only real choice. Read again immediately, or wait a while first?</p> <figure class="blog-fig"> <svg viewBox="0 0 640 270" role="img" aria-label="Two sources publish through a writer into shared memory, a decision process reads, computes, and delivers to a monitor" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2pl-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <circle cx="48" cy="80" r="15" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <circle cx="48" cy="160" r="15" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="48" y="52" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Source 1</text> <text x="48" y="196" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Source 2</text> <line x1="63" y1="86" x2="106" y2="108" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2pl-a)"/> <line x1="63" y1="154" x2="106" y2="132" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2pl-a)"/> <rect x="110" y="98" width="76" height="44" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="148" y="125" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Writer</text> <line x1="186" y1="120" x2="222" y2="120" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2pl-a)"/> <rect x="226" y="86" width="104" height="68" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="226" y1="120" x2="330" y2="120" style="stroke: var(--fig-line); stroke-width: 0.75"/> <text x="278" y="108" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">source 1 update</text> <text x="278" y="140" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">source 2 update</text> <text x="278" y="76" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">shared memory</text> <text x="278" y="172" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">age in memory</text> <line x1="330" y1="120" x2="376" y2="120" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2pl-a)"/> <text x="353" y="136" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">read pair</text> <rect x="380" y="94" width="120" height="52" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="440" y="116" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Decision</text> <text x="440" y="133" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">process</text> <text x="440" y="166" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">compute for T,</text> <text x="440" y="181" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">then wait W</text> <text x="440" y="80" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">age at the reader</text> <line x1="500" y1="120" x2="546" y2="120" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#s2pl-a)"/> <circle cx="578" cy="120" r="26" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="578" y="125" text-anchor="middle" font-size="12" style="fill: var(--fig-line)">Monitor</text> <text x="578" y="166" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">age at the monitor</text> <text x="320" y="232" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">the sources publish on their own schedule, the reader only chooses when to sample</text> </svg> <figcaption>The publish-subscribe loop. Sources publish on their own schedule, the decision process chooses only when to read and compute, and the monitor receives the finished decision updates.</figcaption> </figure> <p>Reading again immediately is the zero-wait policy, and it feels obviously right. The processor never idles, every cycle is spent turning data into decisions, and any deliberate delay looks like negligence. The paper shows that this instinct is wrong, and it shows exactly why.</p> <p>The analysis tracks three coupled age processes. There is the age of the source updates sitting in memory, the age of the sample the decision process is computing on, and the age of the decision update at the monitor. The decision process controls none of the publishing. Sources update memory on their own schedule, so laziness cannot make the inputs fresher. What it can do is change which inputs get sampled and when the computation lands.</p> <p>The cleanest result in the paper is a separation. For any waiting rule that depends only on the previous computation time, the average age at the monitor equals the average age at the reader’s input plus exactly the mean computation time. The output lags the input by \(\mathrm{E}[T]\), no matter how clever or lazy the reading policy is. That lag is a fixed tax, and it means the whole game is minimizing the input age. This reduces our problem to the classic update-or-wait problem, whose solution has a known shape. After a long computation, read again at once. After a short one, wait until a set amount of time has passed since the last read.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="The age at the monitor tracks the age at the reader with a fixed average lag equal to the mean computation time" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2lg-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="60" y1="252" x2="612" y2="252" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2lg-m)"/> <line x1="60" y1="252" x2="60" y2="40" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2lg-m)"/> <text x="340" y="282" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">time</text> <text x="38" y="146" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 38 146)">average age</text> <rect x="80" y="52" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="104" y="58" font-size="12" style="fill: var(--fig-line)">age at the reader</text> <rect x="235" y="52" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="259" y="58" font-size="12" style="fill: var(--fig-line)">age at the monitor</text> <path d="M80 210 L170 168 L170 218 L280 166 L280 222 L400 158 L400 220 L510 170 L510 224 L580 196" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <path d="M80 150 L170 108 L170 158 L280 106 L280 162 L400 98 L400 160 L510 110 L510 164 L580 136" fill="none" style="stroke: var(--fig-rwl); stroke-width: 2"/> <line x1="455" y1="140" x2="455" y2="190" style="stroke: var(--fig-line); stroke-width: 1.25" marker-start="url(#s2lg-m)" marker-end="url(#s2lg-m)"/> <text x="470" y="78" text-anchor="middle" font-size="12" font-weight="600" style="fill: var(--fig-line)">the gap is exactly the mean computation time</text> <line x1="464" y1="84" x2="456" y2="134" style="stroke: var(--fig-muted); stroke-width: 0.8"/> </svg> <figcaption>The separation result. Whatever the reading policy, the average age at the monitor sits exactly one mean computation time above the average age at the reader&rsquo;s input. Illustrative.</figcaption> </figure> <p>Whether waiting is worth much depends on the variance of the computation time. When computation times are exponential, the gains are modest. When they are log-normal with a heavy tail, which is much closer to how real compute behaves on a shared machine, zero-wait becomes clearly suboptimal. In our numerical evaluations at the heaviest tail we tested, the best lazy policy cut the average age at the monitor by roughly a third relative to reading at full tilt, and the gap widens as the tail grows. The intuition is that a long computation already delivered a stale result, and immediately grabbing the next sample tends to repeat the mistake. Waiting lets the memory refresh so the next computation starts from better inputs.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 330" role="img" aria-label="With low variance in computation time zero-wait is nearly optimal, with high variance it is clearly suboptimal" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="s2vr-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="262" x2="612" y2="262" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2vr-m)"/> <line x1="70" y1="262" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#s2vr-m)"/> <text x="340" y="292" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">reading rate</text> <text x="46" y="150" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 150)">average age at the monitor</text> <line x1="575" y1="262" x2="575" y2="78" stroke-dasharray="3 4" style="stroke: var(--fig-muted); stroke-width: 1"/> <text x="612" y="56" text-anchor="end" font-size="11.5" style="fill: var(--fig-muted)">zero-wait,</text> <text x="612" y="70" text-anchor="end" font-size="11.5" style="fill: var(--fig-muted)">read the instant computing ends</text> <rect x="90" y="50" width="18" height="4" rx="2" style="fill: var(--fig-line)"/> <text x="114" y="56" font-size="12" style="fill: var(--fig-line)">low variance</text> <rect x="205" y="50" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="229" y="56" font-size="12" style="fill: var(--fig-line)">moderate</text> <rect x="295" y="50" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="319" y="56" font-size="12" style="fill: var(--fig-line)">heavy tail</text> <path d="M90 120 C230 190 430 225 575 232" fill="none" style="stroke: var(--fig-line); stroke-width: 2"/> <path d="M90 105 C230 170 400 195 490 193 C530 190 560 182 575 176" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <path d="M90 90 C220 150 340 168 420 165 C480 160 540 120 575 88" fill="none" style="stroke: var(--fig-rwl); stroke-width: 2"/> <circle cx="400" cy="165" r="4.5" style="fill: var(--fig-rwl)"/> <text x="395" y="145" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">best lazy policy</text> <text x="165" y="228" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">reads too rare,</text> <text x="165" y="243" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">age dominated by waiting</text> </svg> <figcaption>An illustrative version of the paper&rsquo;s numerical results. With low variance in computation times, zero-wait is nearly optimal. With heavy tails, the best lazy policy is clearly better.</figcaption> </figure> <div class="key-result">However the reader schedules its reads, the monitor lags the input by exactly the mean computation time. All a policy can improve is the input age, and under heavy-tailed computation times lazy reading improved it by roughly a third.</div> <p>The memory side of the analysis produced two findings worth keeping. Because the writer in our model holds no queue, pushing updates at a higher total rate only helps freshness. And the max-age of a sampled pair is penalized by asymmetry between the sources. A pair is only as fresh as its stalest member, so one slow sensor quietly ages every decision computed from the pair.</p> <p>The lesson I took from this paper is that utilization and timeliness are different objectives that only look aligned. A processor that never rests can serve its monitor worse than one that waits on purpose. But the reader in this model was blind. It never looked at the age of what was in memory, only at its own clock. Give the reader eyes and a price per read, and you can ask a sharper question. Exactly when should it read? That is the <a href="/blog/2026/the-best-time-to-read-memory/">second post</a> in this series.</p> <div class="series-nav-footer"> <span> </span> <span> <a href="/blog/2026/the-best-time-to-read-memory/">The best time to read memory</a> &rarr; </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="update-processing"/><category term="queueing"/><summary type="html"><![CDATA[Part one of a second series on my PhD work. Why a decision process that reads shared data and computes on it should sometimes sit idle before reading again.]]></summary></entry><entry><title type="html">Freshness has a memory bill</title><link href="https://vishakha-ramani.github.io/blog/2026/freshness-has-a-memory-bill/" rel="alternate" type="text/html" title="Freshness has a memory bill"/><published>2026-07-17T00:00:00+00:00</published><updated>2026-07-17T00:00:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/freshness-has-a-memory-bill</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/freshness-has-a-memory-bill/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Freshness in shared memory · Part 3 of 3</span><br/> <a href="/blog/2026/a-lock-can-make-data-fresher/">A lock can make data fresher</a> · <a href="/blog/2026/the-router-that-measured-freshness/">The router that measured freshness</a> · Freshness has a memory bill </div> <p>Consider a phone running visual SLAM, the technique that lets a device build a map of its surroundings while tracking its own position inside that map. Camera frames arrive continuously, and concurrent modules for tracking, mapping, and optimization all work against one shared global map in real time. Read-Copy-Update is an attractive way to coordinate them, because a module can pin the current version of the map and compute against it without ever blocking the writer. But every pinned version is a copy sitting in the memory of a device that does not have much to spare.</p> <p>The first two posts in this series established that RCU often delivers fresher information than a lock, and that the advantage shows up on real hardware. Both also hinted at the price, because RCU buys its freshness with copies. A writer publishes a new version, readers still inside their critical sections keep the old one alive, and nothing can be reclaimed until the last of those readers lets go. In <code class="language-plaintext highlighter-rouge">Age-Memory Trade-off in Read-Copy-Update</code>, again with Jiachen Chen and Roy Yates, we asked whether that price is bounded. If updates keep arriving and old versions must survive their readers, can the number of retained copies grow without limit?</p> <p>We made the writer as aggressive as the mechanism allows. In the model, it begins writing the next update the instant it publishes the previous one, with exponential write times, so fresh versions are published as a Poisson process at rate \(\alpha\). This is deliberately the worst case for memory. Read requests arrive as a Poisson process at rate \(\lambda\), and each holds its version for an exponentially distributed read time with mean \(1/\mu\), and a version is reclaimed only when its last reader finishes. One subtlety makes the analysis more than a textbook exercise. How long a version stays current depends on when the next update happens to be published, so the population of live versions is not the \(M/M/\infty\) queue it superficially resembles.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 370" role="img" aria-label="RCU version chain with pinned readers and the sawtooth age of the current update" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p3v-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <path d="M170 52 v-8 h300 v8" fill="none" style="stroke: var(--fig-muted); stroke-width: 1"/> <text x="320" y="34" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">alive until their last reader finishes</text> <rect x="60" y="62" width="80" height="34" rx="5" stroke-dasharray="4 3" style="fill: none; stroke: var(--fig-muted); stroke-width: 1.25"/> <text x="100" y="84" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">v1</text> <text x="100" y="116" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">reclaimed</text> <rect x="170" y="62" width="80" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <text x="210" y="84" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">v2</text> <rect x="280" y="62" width="80" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <text x="320" y="84" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">v3</text> <rect x="390" y="62" width="80" height="34" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <text x="430" y="84" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">v4</text> <rect x="500" y="62" width="80" height="34" rx="5" style="fill: var(--fig-rcu)"/> <text x="540" y="84" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-surface)">v5</text> <text x="540" y="116" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">current</text> <line x1="198" y1="96" x2="198" y2="134" style="stroke: var(--fig-muted); stroke-width: 1"/> <circle cx="198" cy="142" r="7" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="222" y1="96" x2="222" y2="159" style="stroke: var(--fig-muted); stroke-width: 1"/> <circle cx="222" cy="167" r="7" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="320" y1="96" x2="320" y2="134" style="stroke: var(--fig-muted); stroke-width: 1"/> <circle cx="320" cy="142" r="7" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="430" y1="96" x2="430" y2="150" style="stroke: var(--fig-muted); stroke-width: 1"/> <circle cx="430" cy="158" r="7" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <text x="320" y="200" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">readers pin the copy they started with</text> <line x1="210" y1="222" x2="210" y2="332" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.7"/> <line x1="320" y1="222" x2="320" y2="332" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.7"/> <line x1="430" y1="222" x2="430" y2="332" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.7"/> <line x1="540" y1="222" x2="540" y2="332" stroke-dasharray="2 4" style="stroke: var(--fig-muted); stroke-width: 0.7"/> <line x1="60" y1="332" x2="600" y2="332" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#p3v-m)"/> <line x1="60" y1="332" x2="60" y2="236" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#p3v-m)"/> <text x="68" y="232" font-size="11.5" style="fill: var(--fig-muted)">age of the current update</text> <path d="M100 322 L210 268 L210 322 L320 268 L320 322 L430 268 L430 322 L540 268 L540 322 L590 298" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <text x="330" y="356" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">each publish resets the age, faster writing means a lower sawtooth</text> </svg> <figcaption>RCU&rsquo;s version chain. Readers keep old copies alive through their grace periods while the age of the current update follows a sawtooth. Illustrative.</figcaption> </figure> <p>Two results summarize the paper. First, the average age of the current update in memory is \(2/\alpha\) for write rate \(\alpha\), so a faster writer keeps memory fresher, with no surprise there. Second, the average number of updates held in the system never exceeds \(1 + \lambda/\mu\), where \(\lambda\) is the read request rate and \(1/\mu\) is the mean read time, no matter how fast the writer publishes. Writing faster makes information fresher without blowing up the version count. The bound also has a clean interpretation in the limit of an infinitely fast writer. Each read request then effectively pins its own private version, the pinned versions do form an \(M/M/\infty\) queue, and their number is Poisson with mean \(\lambda/\mu\). Add the single current version and the bound is met exactly.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 320" role="img" aria-label="Faster writing lowers age while memory approaches its ceiling" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p3t-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> <pattern id="p3t-hatch" width="12" height="12" patternTransform="rotate(45)" patternUnits="userSpaceOnUse"> <line x1="0" y1="0" x2="0" y2="12" style="stroke: var(--fig-muted); stroke-width: 1" opacity="0.35"/> </pattern> </defs> <line x1="80" y1="250" x2="612" y2="250" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p3t-m)"/> <line x1="80" y1="250" x2="80" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p3t-m)"/> <text x="340" y="282" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">average number of versions in memory</text> <text x="54" y="145" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 54 145)">average age in memory</text> <rect x="470" y="55" width="130" height="195" style="fill: url(#p3t-hatch)"/> <text x="535" y="150" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">unreachable</text> <line x1="470" y1="250" x2="470" y2="55" stroke-dasharray="6 4" style="stroke: var(--fig-line); stroke-width: 1.4"/> <text x="470" y="44" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">ceiling 1 + &#955;/&#956;</text> <path d="M95 72 C220 100 380 165 462 242" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <text x="230" y="128" font-size="12" style="fill: var(--fig-muted)">writer publishes faster</text> <line x1="290" y1="140" x2="336" y2="163" style="stroke: var(--fig-muted); stroke-width: 1.1" marker-end="url(#p3t-m)"/> </svg> <figcaption>The trade-off in one curve, after the paper&rsquo;s numerical evaluation. Faster writing makes memory fresher while the version count approaches its ceiling.</figcaption> </figure> <div class="key-result">Freshness has a bounded bill. However fast the writer publishes, the average number of retained versions never exceeds 1 + &#955;/&#956;.</div> <p>The satisfying part is where the bill lands. Memory consumption in RCU is governed by the read side, not the write side. Slow reads and frequent read requests are what hold versions in memory, so a system that needs to cap RCU’s footprint should shorten its read-side critical sections or consolidate its read requests, rather than throttle the writer and pay for the savings in staleness.</p> <p>Taken together, the three papers form one argument. Synchronization primitives are not interchangeable implementation details, because they decide which version of the truth a reader observes. Their effects on freshness are measurable in a real forwarder, alongside timeliness penalties that throughput benchmarks never reveal. And the freshest mechanism carries a resource cost that can be quantified and belongs in the model next to the age it buys.</p> <p>I now work on performance modeling and analysis for large language model inference at IBM Research, and the same accounting follows me there. An inference server holds key-value cache in GPU memory so that tokens keep streaming on time, and the question of how much memory a latency target costs is asked in gigabytes rather than table versions. The setting has changed, but the tradeoff has not.</p> <div class="series-nav-footer"> <span> &larr; <a href="/blog/2026/the-router-that-measured-freshness/">The router that measured freshness</a> </span> <span> </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="rcu"/><category term="shared-memory"/><summary type="html"><![CDATA[Part three of a series on my PhD work. How much memory Read-Copy-Update spends to keep information fresh, and why the bill stays bounded.]]></summary></entry><entry><title type="html">The router that measured freshness</title><link href="https://vishakha-ramani.github.io/blog/2026/the-router-that-measured-freshness/" rel="alternate" type="text/html" title="The router that measured freshness"/><published>2026-07-16T00:00:00+00:00</published><updated>2026-07-16T00:00:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/the-router-that-measured-freshness</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/the-router-that-measured-freshness/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Freshness in shared memory · Part 2 of 3</span><br/> <a href="/blog/2026/a-lock-can-make-data-fresher/">A lock can make data fresher</a> · The router that measured freshness · <a href="/blog/2026/freshness-has-a-memory-bill/">Freshness has a memory bill</a> </div> <p>There is a practical problem at the heart of measuring information freshness, and it nearly sank this experiment before it started. The age of a received update is the gap between the receiver’s clock and the timestamp the sender stamped into the packet. On our testbed, NTP synchronizes clocks to about a millisecond, but the effects we needed to observe live at microsecond scale. Measured naively, the data would have been mostly clock error. Our workaround was to place the sender and the receiver on the same machine, so that every timestamp and every arrival referenced a single clock.</p> <p>The experiment, described in <code class="language-plaintext highlighter-rouge">Timely Mobile Routing: An Experimental Study</code> with Jiachen Chen and Roy Yates, is the empirical companion to the analysis in <a href="/blog/2026/a-lock-can-make-data-fresher/">part one</a>. The stochastic models predicted that the choice between Read-Copy-Update and a Readers-Writer Lock changes the freshness of delivered information. We wanted to know whether that prediction survives contact with real hardware.</p> <p>On the COSMOS testbed, we connected two machines over 25 Gbps links and built the forwarding application in DPDK, a kernel-bypass packet processing framework used in data centers and core routers. The source machine plays both the application server and 1000 mobile users at once. It emits two interleaved streams, data packets carrying timestamped application updates and control packets announcing user movements, with user popularity drawn from a Zipf distribution. The forwarder keeps its forwarding table in a hash table protected by either RCU or RWL. For each data packet it looks up the user, stamps the address it found, and sends the packet back to be scored as correctly delivered or misaddressed.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="Packet forwarding testbed with sender and receiver sharing one clock" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p2tb-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <rect x="20" y="40" width="250" height="220" rx="8" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="145" y="64" text-anchor="middle" font-size="13.5" font-weight="600" style="fill: var(--fig-line)">Source machine</text> <rect x="45" y="85" width="120" height="44" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <text x="105" y="112" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Sender thread</text> <rect x="45" y="185" width="120" height="44" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <text x="105" y="212" text-anchor="middle" font-size="12.5" style="fill: var(--fig-line)">Receiver thread</text> <circle cx="215" cy="157" r="16" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <line x1="215" y1="157" x2="215" y2="146" style="stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="215" y1="157" x2="223" y2="161" style="stroke: var(--fig-line); stroke-width: 1.25"/> <text x="215" y="196" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">one clock</text> <line x1="201" y1="148" x2="168" y2="115" stroke-dasharray="2 3" style="stroke: var(--fig-muted); stroke-width: 0.9"/> <line x1="201" y1="166" x2="168" y2="199" stroke-dasharray="2 3" style="stroke: var(--fig-muted); stroke-width: 0.9"/> <text x="320" y="76" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">25 Gbps</text> <line x1="272" y1="95" x2="388" y2="95" stroke-dasharray="5 4" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p2tb-a)"/> <text x="320" y="88" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">location updates</text> <line x1="272" y1="130" x2="388" y2="130" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p2tb-a)"/> <text x="320" y="122" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">app updates</text> <line x1="366" y1="207" x2="276" y2="207" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p2tb-a)"/> <text x="320" y="226" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">addressed updates return</text> <rect x="370" y="40" width="250" height="220" rx="8" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="495" y="64" text-anchor="middle" font-size="13.5" font-weight="600" style="fill: var(--fig-line)">Forwarder</text> <rect x="392" y="85" width="72" height="20" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="410" y1="85" x2="410" y2="105" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="428" y1="85" x2="428" y2="105" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="446" y1="85" x2="446" y2="105" style="stroke: var(--fig-line); stroke-width: 0.75"/> <text x="428" y="121" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">control ring</text> <rect x="392" y="185" width="72" height="20" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="410" y1="185" x2="410" y2="205" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="428" y1="185" x2="428" y2="205" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="446" y1="185" x2="446" y2="205" style="stroke: var(--fig-line); stroke-width: 0.75"/> <text x="428" y="221" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">data ring</text> <text x="560" y="112" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">FIB</text> <rect x="525" y="120" width="70" height="70" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="525" y1="143" x2="595" y2="143" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="525" y1="166" x2="595" y2="166" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="466" y1="102" x2="521" y2="136" stroke-dasharray="5 4" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p2tb-a)"/> <text x="480" y="140" font-size="10.5" style="fill: var(--fig-muted)">write</text> <line x1="466" y1="188" x2="521" y2="174" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p2tb-a)"/> <text x="480" y="200" font-size="10.5" style="fill: var(--fig-muted)">read</text> </svg> <figcaption>The testbed. Sender and receiver share one machine and one clock so that microsecond ages are measurable, while the forwarder serves both packet streams around a shared hash table.</figcaption> </figure> <p>Before testing any synchronization, we ran a baseline with a single immobile user and no table lookups at all, just to understand the packet path. Even that produced a lesson. Average age traced a U shape against sending rate. Up to about 1 million packets per second, sending faster made information fresher, as expected. Beyond roughly 4 million packets per second, age began climbing again. The culprit was batching. DPDK fills transmission rings in bursts to maximize throughput, and once the offered rate is high enough, packets start sharing batches and queueing in the ring while their information grows old. A framework tuned entirely for throughput was quietly taxing timeliness. The scale of the effect grew with the workload too. Average ages of 1 to 10 microseconds in the one-user baseline became 5 to 10 milliseconds once the experiment averaged over 1000 users.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="Average age against sending rate traces a U shape" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p2u-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="242" x2="612" y2="242" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p2u-m)"/> <line x1="70" y1="242" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p2u-m)"/> <text x="340" y="272" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">sending rate</text> <text x="46" y="140" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 140)">average age at the receiver</text> <line x1="380" y1="242" x2="380" y2="70" stroke-dasharray="3 4" style="stroke: var(--fig-muted); stroke-width: 1"/> <text x="380" y="58" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">about 4 Mpps on our testbed</text> <path d="M90 95 C160 160 210 182 265 183 C340 183 480 120 585 72" fill="none" style="stroke: var(--fig-line); stroke-width: 2"/> <circle cx="265" cy="183" r="4.5" style="fill: var(--fig-line)"/> <text x="265" y="207" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">freshest point</text> <text x="215" y="82" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">more frequent updates,</text> <text x="215" y="98" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">fresher information</text> <text x="495" y="170" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">packets queue in batches,</text> <text x="495" y="186" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">information ages</text> </svg> <figcaption>An illustrative version of the baseline result. Age falls as updates come more often, then batching turns extra speed into queueing.</figcaption> </figure> <figure class="blog-fig"> <svg viewBox="0 0 640 190" role="img" aria-label="Below the knee packets ship alone, above it they age while a batch fills" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p2b-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <line x1="320" y1="20" x2="320" y2="160" style="stroke: var(--fig-muted); stroke-width: 0.5"/> <text x="160" y="32" text-anchor="middle" font-size="13" font-weight="600" style="fill: var(--fig-line)">Below the knee</text> <rect x="52" y="70" width="22" height="22" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="80" y1="81" x2="102" y2="81" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p2b-a)"/> <rect x="112" y="70" width="22" height="22" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="140" y1="81" x2="162" y2="81" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p2b-a)"/> <rect x="172" y="70" width="22" height="22" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="200" y1="81" x2="222" y2="81" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p2b-a)"/> <rect x="232" y="60" width="44" height="44" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="254" y="87" text-anchor="middle" font-size="11.5" style="fill: var(--fig-line)">NIC</text> <text x="160" y="140" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">each packet goes out at once</text> <text x="480" y="32" text-anchor="middle" font-size="13" font-weight="600" style="fill: var(--fig-line)">Above the knee</text> <rect x="350" y="52" width="146" height="62" rx="6" stroke-dasharray="5 4" style="fill: none; stroke: var(--fig-muted); stroke-width: 1.25"/> <rect x="358" y="58" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="388" y="58" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="418" y="58" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="448" y="58" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="358" y="86" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <rect x="388" y="86" width="20" height="20" rx="4" style="fill: none; stroke: var(--fig-line); stroke-width: 1.4"/> <line x1="502" y1="83" x2="536" y2="83" style="stroke: var(--fig-line); stroke-width: 2.5" marker-end="url(#p2b-a)"/> <rect x="546" y="61" width="44" height="44" rx="5" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="568" y="88" text-anchor="middle" font-size="11.5" style="fill: var(--fig-line)">NIC</text> <text x="423" y="140" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">age grows while the batch fills</text> <text x="568" y="128" text-anchor="middle" font-size="11" style="fill: var(--fig-muted)">one burst</text> </svg> <figcaption>Why the right half of the U bends upward. Below the knee every packet ships alone. Above it, packets age while a batch fills.</figcaption> </figure> <p>The synchronization results matched the analysis where it counted. With one control packet per hundred data packets, RCU’s cheap read side generally delivered fresher application updates than RWL at high sending rates, while at low rates the difference was negligible. When we raised the update ratio tenfold, both primitives struggled. The control ring backed up, the table lagged behind the users’ actual movements, and misaddressed packets multiplied, with RWL misdelivering more than RCU. Under RWL, average age even reproduced the classic curve from updating theory, first falling with sending rate and then rising as the system congested. Updating should be fast, but not too fast.</p> <div class="key-result">RCU generally delivered fresher updates than RWL at high sending rates, and the biggest timeliness tax came from throughput-oriented batching rather than from locks.</div> <p>For me, the testbed’s job was to keep the theory honest, and it did more than that. It confirmed the effect the models predicted, and it exposed penalties no clean model contained, like the batch admission delay. A forwarder can look excellent in packets per second while quietly delivering addresses that are no longer true, which is why freshness has to be measured directly rather than inferred from throughput or latency.</p> <p>The experiments also made RCU’s hidden cost tangible. Every RCU write creates another copy of the table entry, and stale copies linger until their readers finish, so the memory footprint grows and the implementation needs its own reclamation machinery. That raised a question the testbed could not answer. If a writer publishes updates as fast as it likes, does the pile of retained versions stay bounded? Answering that took another paper, and it is the subject of the <a href="/blog/2026/freshness-has-a-memory-bill/">final post</a> in this series.</p> <div class="series-nav-footer"> <span> &larr; <a href="/blog/2026/a-lock-can-make-data-fresher/">A lock can make data fresher</a> </span> <span> <a href="/blog/2026/freshness-has-a-memory-bill/">Freshness has a memory bill</a> &rarr; </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="networking"/><category term="experiments"/><summary type="html"><![CDATA[Part two of a series on my PhD work. A DPDK packet forwarding testbed that measures the age of the information it delivers.]]></summary></entry><entry><title type="html">A lock can make data fresher</title><link href="https://vishakha-ramani.github.io/blog/2026/a-lock-can-make-data-fresher/" rel="alternate" type="text/html" title="A lock can make data fresher"/><published>2026-07-15T00:00:00+00:00</published><updated>2026-07-15T00:00:00+00:00</updated><id>https://vishakha-ramani.github.io/blog/2026/a-lock-can-make-data-fresher</id><content type="html" xml:base="https://vishakha-ramani.github.io/blog/2026/a-lock-can-make-data-fresher/"><![CDATA[<div class="series-nav"> <span class="series-nav-title">Freshness in shared memory · Part 1 of 3</span><br/> A lock can make data fresher · <a href="/blog/2026/the-router-that-measured-freshness/">The router that measured freshness</a> · <a href="/blog/2026/freshness-has-a-memory-bill/">Freshness has a memory bill</a> </div> <p>Somewhere in the network sits a forwarder whose job is to keep up with a moving target. An application server streams updates to a mobile user, the user changes their point of attachment as they move, and the forwarder maintains a table that records where the user currently is. Every application packet passing through consults that table for an address.</p> <p>Two processes contend for the table. A writer records fresh locations as the user moves. A reader looks up addresses so application packets can be delivered. When a read returns a stale location, the packet is sent to the wrong place, and in our model a misaddressed packet is simply lost. Whether the user receives timely updates therefore depends on a piece of machinery most performance models never mention. It depends on how the table is locked.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 300" role="img" aria-label="Update forwarding system with a shared forwarding table" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p1sys-a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-line)"/> </marker> </defs> <rect x="20" y="115" width="130" height="64" rx="6" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="85" y="142" text-anchor="middle" font-size="13" style="fill: var(--fig-line)">Application</text> <text x="85" y="160" text-anchor="middle" font-size="13" style="fill: var(--fig-line)">server</text> <line x1="150" y1="147" x2="214" y2="147" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p1sys-a)"/> <text x="182" y="136" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">app updates</text> <rect x="220" y="58" width="220" height="176" rx="8" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <text x="330" y="82" text-anchor="middle" font-size="13.5" font-weight="600" style="fill: var(--fig-line)">Forwarder</text> <text x="345" y="106" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">FIB</text> <rect x="285" y="112" width="120" height="78" rx="3" style="fill: none; stroke: var(--fig-line); stroke-width: 1.25"/> <line x1="285" y1="138" x2="405" y2="138" style="stroke: var(--fig-line); stroke-width: 0.75"/> <line x1="285" y1="164" x2="405" y2="164" style="stroke: var(--fig-line); stroke-width: 0.75"/> <text x="295" y="129" font-size="10.5" style="fill: var(--fig-muted)">user 101 &#8594; addr</text> <text x="295" y="155" font-size="10.5" style="fill: var(--fig-muted)">user 102 &#8594; addr</text> <text x="295" y="181" font-size="10.5" style="fill: var(--fig-muted)">user 103 &#8594; addr</text> <line x1="234" y1="147" x2="281" y2="147" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p1sys-a)"/> <text x="257" y="167" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">read</text> <line x1="345" y1="230" x2="345" y2="194" stroke-dasharray="5 4" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p1sys-a)"/> <text x="354" y="216" font-size="10.5" style="fill: var(--fig-muted)">write</text> <line x1="440" y1="147" x2="516" y2="147" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p1sys-a)"/> <text x="478" y="136" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">delivered</text> <rect x="520" y="100" width="58" height="96" rx="10" style="fill: none; stroke: var(--fig-line); stroke-width: 1.5"/> <line x1="540" y1="182" x2="558" y2="182" style="stroke: var(--fig-line); stroke-width: 1.5"/> <text x="549" y="90" text-anchor="middle" font-size="13" style="fill: var(--fig-line)">Mobile user</text> <path d="M588 122 q9 25 0 50" style="fill: none; stroke: var(--fig-muted); stroke-width: 1.5"/> <path d="M598 112 q13 35 0 70" style="fill: none; stroke: var(--fig-muted); stroke-width: 1.25"/> <path d="M549 205 C549 268 440 270 352 240" fill="none" stroke-dasharray="5 4" style="stroke: var(--fig-line); stroke-width: 1.5" marker-end="url(#p1sys-a)"/> <text x="465" y="288" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">location updates as the user moves</text> </svg> <figcaption>The forwarding scenario. Location updates write to the table while app updates read it, and both streams meet at the FIB.</figcaption> </figure> <p>In <code class="language-plaintext highlighter-rouge">Lock-based or Lock-less: Which Is Fresh?</code>, written with Jiachen Chen and Roy Yates during my PhD at Rutgers, we compared the two standard ways of protecting such a table. A Readers-Writer Lock enforces mutual exclusion, so a writer holding the lock blocks readers until it finishes. Read-Copy-Update takes the opposite view. Readers never block. The writer prepares a fresh copy of the data and atomically swaps in a pointer to it, while readers already in flight continue using the old version until they are done.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 260" role="img" aria-label="The same read request under RWL and RCU" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p1mech-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <g> <rect x="12" y="12" width="26" height="5" rx="2.5" style="fill: var(--fig-rwl)"/> <text x="46" y="20" font-size="13.5" font-weight="600" style="fill: var(--fig-line)">Readers-Writer Lock</text> <text x="12" y="72" font-size="12" style="fill: var(--fig-muted)">writer</text> <line x1="62" y1="68" x2="298" y2="68" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <rect x="105" y="55" width="90" height="25" rx="4" style="fill: var(--fig-rwl)"/> <text x="150" y="46" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">holds the write lock</text> <text x="12" y="132" font-size="12" style="fill: var(--fig-muted)">reader</text> <line x1="62" y1="128" x2="298" y2="128" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <line x1="130" y1="98" x2="130" y2="112" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p1mech-m)"/> <text x="130" y="94" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">request</text> <rect x="130" y="115" width="65" height="25" rx="4" stroke-dasharray="4 3" style="fill: var(--fig-surface); stroke: var(--fig-muted); stroke-width: 1.25"/> <text x="162" y="131" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">waits</text> <rect x="195" y="115" width="50" height="25" rx="4" style="fill: var(--fig-rwl)"/> <line x1="62" y1="170" x2="290" y2="170" style="stroke: var(--fig-muted); stroke-width: 1" marker-end="url(#p1mech-m)"/> <text x="288" y="186" text-anchor="end" font-size="11" style="fill: var(--fig-muted)">time</text> <text x="155" y="205" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">reads the fresh address</text> <text x="155" y="226" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">app update delivered correctly</text> </g> <g transform="translate(330 0)"> <rect x="12" y="12" width="26" height="5" rx="2.5" style="fill: var(--fig-rcu)"/> <text x="46" y="20" font-size="13.5" font-weight="600" style="fill: var(--fig-line)">Read-Copy-Update</text> <text x="12" y="72" font-size="12" style="fill: var(--fig-muted)">writer</text> <line x1="62" y1="68" x2="298" y2="68" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <rect x="105" y="55" width="90" height="25" rx="4" style="fill: var(--fig-rcu)"/> <text x="150" y="46" text-anchor="middle" font-size="11.5" style="fill: var(--fig-muted)">prepares a new copy</text> <text x="12" y="132" font-size="12" style="fill: var(--fig-muted)">reader</text> <line x1="62" y1="128" x2="298" y2="128" style="stroke: var(--fig-muted); stroke-width: 0.6"/> <line x1="130" y1="98" x2="130" y2="112" style="stroke: var(--fig-line); stroke-width: 1.25" marker-end="url(#p1mech-m)"/> <text x="130" y="94" text-anchor="middle" font-size="10.5" style="fill: var(--fig-muted)">request</text> <rect x="130" y="115" width="50" height="25" rx="4" style="fill: var(--fig-rcu)"/> <text x="207" y="152" font-size="10.5" style="fill: var(--fig-muted)">no waiting</text> <line x1="62" y1="170" x2="290" y2="170" style="stroke: var(--fig-muted); stroke-width: 1" marker-end="url(#p1mech-m)"/> <text x="288" y="186" text-anchor="end" font-size="11" style="fill: var(--fig-muted)">time</text> <text x="155" y="205" text-anchor="middle" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">reads the old copy</text> <text x="155" y="226" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">app update goes to the old address</text> </g> </svg> <figcaption>The same read request under both primitives. The lock delays the reader and delivers a fresh address. RCU serves the reader at once from the copy that is about to be replaced. Illustrative timelines.</figcaption> </figure> <p>Benchmarked on throughput, this contest has a conventional winner. RCU is celebrated precisely because its read side is cheap, and operations per second favor it in read-mostly workloads. But our metric was not operations per second. We used the Age of Information, which measures how old a piece of information is at the moment it is delivered, and we tracked two quantities. One is the age of the location stored in the table. The other is the age of the application updates that actually reach the user.</p> <p>The two ages are coupled through the table, so we modeled reader and writer together as a stochastic hybrid system. The discrete component is a five state Markov chain describing what the reader and writer are each doing, and the continuous component carries the age processes, which reset differently under RCU and RWL transitions. The payoff of the framework is a closed set of linear age balance equations whose solution gives the average ages without simulation.</p> <p>The answer refused to pick a side. At low location update rates, RCU delivers fresher application updates, exactly as the throughput intuition suggests, because its light read side serves lookups quickly. At high location update rates, the ranking flips. The write lock in RWL, the very thing that makes readers wait, prevents a reader from walking away with an address that is in the middle of being replaced. RCU has no such restraint. Its reader cheerfully reads the old copy while the writer publishes the new one, and the packet goes out misaddressed. Blocking, usually cast as pure overhead, was acting as quality control.</p> <figure class="blog-fig"> <svg viewBox="0 0 640 340" role="img" aria-label="Which primitive is fresher depends on the location update rate" xmlns="http://www.w3.org/2000/svg"> <defs> <marker id="p1reg-m" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <path d="M0 0 L10 5 L0 10 z" style="fill: var(--fig-muted)"/> </marker> </defs> <line x1="70" y1="272" x2="612" y2="272" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p1reg-m)"/> <line x1="70" y1="272" x2="70" y2="38" style="stroke: var(--fig-muted); stroke-width: 1.25" marker-end="url(#p1reg-m)"/> <text x="340" y="300" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)">location update rate</text> <text x="46" y="155" text-anchor="middle" font-size="12.5" style="fill: var(--fig-muted)" transform="rotate(-90 46 155)">average app update age</text> <rect x="86" y="46" width="18" height="4" rx="2" style="fill: var(--fig-rcu)"/> <text x="110" y="52" font-size="12" style="fill: var(--fig-line)">RCU</text> <rect x="150" y="46" width="18" height="4" rx="2" style="fill: var(--fig-rwl)"/> <text x="174" y="52" font-size="12" style="fill: var(--fig-line)">RWL</text> <line x1="430" y1="272" x2="430" y2="90" stroke-dasharray="3 4" style="stroke: var(--fig-muted); stroke-width: 1"/> <path d="M85 215 C260 200 430 110 585 62" fill="none" style="stroke: var(--fig-rcu); stroke-width: 2"/> <path d="M85 168 C260 160 430 145 585 132" fill="none" style="stroke: var(--fig-rwl); stroke-width: 2"/> <rect x="548" y="50" width="14" height="3.5" rx="1.75" style="fill: var(--fig-rcu)"/> <text x="567" y="55" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">RCU</text> <rect x="548" y="115" width="14" height="3.5" rx="1.75" style="fill: var(--fig-rwl)"/> <text x="567" y="120" font-size="12.5" font-weight="600" style="fill: var(--fig-line)">RWL</text> <text x="250" y="252" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">RCU serves fresher updates</text> <text x="522" y="252" text-anchor="middle" font-size="12" style="fill: var(--fig-muted)">RWL serves fresher updates</text> <g> <rect x="440" y="160" width="160" height="70" rx="5" style="fill: none; stroke: var(--fig-muted); stroke-width: 0.75"/> <text x="450" y="177" font-size="11" style="fill: var(--fig-muted)">age cut by preemption</text> <rect x="450" y="186" width="34" height="9" rx="2" style="fill: var(--fig-rcu)"/> <text x="490" y="194" font-size="11" style="fill: var(--fig-line)">15% RCU</text> <rect x="450" y="203" width="102" height="9" rx="2" style="fill: var(--fig-rwl)"/> <text x="558" y="211" font-size="11" style="fill: var(--fig-line)">45% RWL</text> </g> </svg> <figcaption>An illustrative summary of the paper&rsquo;s numerical results. RCU wins when locations change rarely and RWL wins when they change often, while preemption reduces age under both.</figcaption> </figure> <div class="key-result">The freshest primitive is workload dependent. RCU serves fresher app updates when locations change rarely, RWL when they change often, and preemption helps both.</div> <p>The paper also redesigns both processes around preemption. A writer that receives a fresher location abandons the write in progress and starts over with the newer one. A reader lets the newest waiting application update claim the address returned by an in-flight read, rather than serving an older packet. In our numerical evaluations, preemption reduced the average application update age by close to 15 percent under RCU and 45 percent under RWL.</p> <p>The result I keep returning to is not that one primitive won. It is that the synchronization primitive belongs inside the performance model rather than underneath it. The mechanism decides which update a reader actually observes, and no amount of downstream optimization can recover information that was already stale when it was read. That said, this conclusion rested on exponential service times and other modeling conveniences. The obvious challenge was to find out whether real hardware agrees, so we built a packet forwarder that measures its own freshness. That experiment is the subject of the <a href="/blog/2026/the-router-that-measured-freshness/">second post</a> in this series.</p> <div class="series-nav-footer"> <span> </span> <span> <a href="/blog/2026/the-router-that-measured-freshness/">The router that measured freshness</a> &rarr; </span> </div> <p>Vishakha Ramani is a Research Staff Member at IBM Research. She works on performance modeling and analysis of large-scale computing systems, most recently for large language model inference. She holds a PhD from Rutgers University, where she studied the timeliness of information in computing systems with Prof. Roy Yates. <a href="https://scholar.google.com/citations?user=eJg7gqwAAAAJ">Google Scholar</a> · <a href="https://www.linkedin.com/in/vishakha-ramani/">LinkedIn</a></p>]]></content><author><name></name></author><category term="research"/><category term="age-of-information"/><category term="synchronization"/><category term="shared-memory"/><summary type="html"><![CDATA[Part one of a series on my PhD work. Comparing Read-Copy-Update and Readers-Writer locks when the metric is information freshness rather than throughput.]]></summary></entry></feed>