Skip to content
VLSI Mentor

DDR · Module 23

Controller Policies

Every chapter assumed a row stays open after a burst. That is a decision, its crossover is exactly tRCD over tRP plus tRCD, and an adaptive policy that mispredicts is worse than both fixed choices.

Every chapter in this module has assumed something without naming it: that a row, once opened, stays open until another request needs the bank.

That is a policy, not a fact. Chapter 23.3 §2's cost model charges a different-row access tRP + tRCD = 22 cycles precisely because the old row was still open when the new request arrived. Had the controller closed it when the previous burst finished, the same access would have been a miss at tRCD = 11 cycles — half the cost.

So there is a decision nobody has made yet, and it is made once per burst: when a burst completes, close the row or leave it open?

The module's central law, for the last time:

Peak bandwidth is a property of the interface. Achieved bandwidth is a property of the workload meeting the timing rules. The gap between them is not waste — it is the cost of constraints that cannot be removed, plus decisions that can be improved.

This chapter is the purest instance of the second clause in the module. The decision costs nothing to make, changes no timing parameter, and moves the cost of a workload by a factor of five — and §3 shows its crossover point is not a measurement but an algebraic consequence of two published numbers.

1. The Decision, and When It Is Made

The decision has a precise moment: the cycle a burst completes. Before that the row must stay open — it is carrying the transfer. After that, two options.

PolicyOn burst completionNext access to that bank, same rowNext access, different row
Open-pageLeave the row openHIT — 0 cyclesCONFLICTtRP + tRCD = 22
Close-pagePrecharge immediatelyMISStRCD = 11MISStRCD = 11

Read the two right-hand columns together, because the whole chapter is in them.

Open-page is a bet that the next access wants the same row. It wins completely when right — zero cycles — and loses badly when wrong, because the precharge it avoided must now happen in the critical path of the new request.

Close-page is a refusal to bet. Its cost is tRCD regardless of what comes next. It never achieves zero and it never pays 22.

So the two policies are not better and worse; they are a gamble and an insurance premium. And §3 shows the break-even odds are computable rather than empirical.

2. Closing After a Write Is Not the Same

One asymmetry before the algebra, because it is easy to miss and it changes the arithmetic for write-heavy workloads.

Chapter 11.4 owns tWR — write recovery time — and it is the interval a device needs after write data before the row may be precharged. VERIFIED at this configuration: tWR = 15 ns = 12 cycles.

So close-page after a read can precharge as soon as tRTP allows — VERIFIED at 6 cycles. Close-page after a write must wait tWR = 12 cycles first.

Preceding accessDelay before precharge may beginCategory
ReadtRTP = 6 cyclesVERIFIED
WritetWR = 12 cyclesVERIFIED

Consequence: close-page's precharge is twice as slow to start after a write, so the window in which it can be hidden (§1's callout) is correspondingly tighter. On a write-heavy workload, close-page's advantage is harder to realise — and a policy engine that ignores the distinction will predict a benefit it does not get.

This is also why the auto-precharge variant of the column command exists (7.4 owns it): the device applies the correct delay itself, so the controller does not have to track which of the two applies.

3. The Crossover Is Algebra, Not Measurement

Here is the chapter's central result, and it is derived rather than simulated.

Let h be the probability that the next access to a bank wants the same row — the hit probability under open-page. Then expected row work per access is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   open-page   =  h x 0  +  (1-h) x (tRP + tRCD)
               =  (1-h) x (tRP + tRCD)

   close-page  =  tRCD                     -- independent of h

Open-page wins when (1-h) × (tRP + tRCD) < tRCD, which rearranges to:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                       tRCD
   h  >  ─────────────────────────
               tRP  +  tRCD

That is the crossover, in closed form. At DDR4-1600K, where tRP = tRCD = 11:

h_crossover = 11 / 22 = 1/2 — exactly 50%.

DERIVED, and it is exact rather than approximate. Two consequences follow immediately.

A workload with better than 50% row-hit probability should use open-page; worse than 50%, close-page. At exactly 50% the two are identical in expectation, which §4's table confirms numerically.

And the threshold is a property of the device, not of the workload. It depends only on tRP and tRCD. So the threshold is fixed at design time from published numbers, and only the measurement of h is workload-dependent — which is precisely the division an adaptive policy needs.

4. Neither Fixed Policy Wins

§3's algebra predicts a crossover. Here it is numerically, over five workload shapes at 1000 accesses each. All DERIVED; h is the stated model parameter.

Workload shapehOpen-pageClose-pageWinnerMargin
High locality90%220011000Open-page8800 cycles
Moderate70%660011000Open-page4400 cycles
At the crossover50%1100011000Exact tie0
Poor30%1540011000Close-page4400 cycles
Low locality10%1980011000Close-page8800 cycles

Four observations, and the last two are the design content.

The tie at 50% is exact, which is §3's algebra confirmed rather than approximated.

The table is symmetric about the crossover. Margins of 8800 and 4400 appear on both sides at h of 90/10 and 70/30 — a consequence of open-page's cost being linear in h while close-page's is constant.

Open-page's range is far wider. It spans 2200 to 19800 cycles — a factor of 9 across the workload range — while close-page is flat at 11000. Open-page is the high-variance policy, and that matters beyond its mean: a system that must bound its worst case has a reason to prefer close-page that this table's winner column does not capture.

And no fixed choice is safe. Choosing open-page costs 8800 cycles on the low-locality workload; choosing close-page costs 8800 on the high-locality one. A fixed policy is a bet on the workload, which is the entire argument for adapting.

5. What Adaptation Can Buy, and Its Ceiling

Before building an adaptive policy it is worth deriving what a perfect one would achieve, because that is the ceiling and it is smaller than it first appears.

A perfect predictor closes the row exactly when the next access to that bank will want a different row, and leaves it open exactly when the next access will want the same row. So:

  • Same-row accesses stay hits — 0 cycles, as under open-page.
  • Different-row accesses become misses at tRCD — not conflicts at tRP + tRCD, because the row was closed in advance.

Expected cost is therefore (1-h) × tRCD. Compare all four options on the high-locality workload, h = 90%, 1000 accesses:

PolicyRow workRelative to perfect
Perfect adaptive1100 cycles1.0×
Open-page22002.0×
Close-page1100010.0×
Thrashing adaptive (§6)1210011.0×

All DERIVED. Two results.

Perfect adaptation beats open-page by exactly 2× on the non-hit accesses, and the reason is structural: it converts every conflict into a miss, and a conflict is (tRP + tRCD) / tRCD = 2× a miss at this bin. That factor is the ceiling on what prediction can buy over open-page, and it is the same ratio 23.3 §2's callout identified.

So adaptation's upside is bounded and known in advance. At h = 90% it is 1100 cycles on 1000 accesses. A predictor that is right 80% of the time captures a proportion of that, and a predictor that is right less than half the time is worse than not predicting — which §6 quantifies.

6. A Policy That Changes Its Mind Is Worse Than Either

§5's table has a fourth row that needs explaining, because it is the failure mode an adaptive policy must be designed against.

Consider an anti-correlated predictor on the same h = 90% workload: it closes the row before every same-row access and leaves it open before every different-row access. Perfectly wrong, every time.

  • The 900 would-be hits become misses at tRCD = 11 → 9900 cycles.
  • The 100 different-row accesses stay conflicts at 22 → 2200 cycles.
  • Total: 12100 cycles.

DERIVED. That is 5.5× worse than open-page, 1.1× worse than close-page, and 11× worse than a perfect predictor on the identical workload.

A wrong adaptive policy is worse than either fixed policy, and the reason is that it combines both failure modes: it pays close-page's premium on the accesses where open-page would have been free, and open-page's penalty on the accesses where close-page would have been cheap.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                        same-row access   different-row access
   open-page            0   (best)        22  (worst)
   close-page           11                11
   perfect adaptive     0   (best)        11  (best)
   anti-correlated      11                22  (worst)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        worst of both columns

That table is the argument for hysteresis. A predictor with no memory, reacting to the most recent outcome, is not merely imprecise — on an alternating access pattern it can be systematically anti-correlated, because the thing it just observed is exactly the wrong guide to what comes next.

7. What the Predictor Must Predict

It is worth being precise, because the obvious formulation is wrong in a way that matters.

The wrong formulation: predict whether this bank's hit rate is above the crossover. That is a per-bank aggregate, it changes slowly, and by the time it has moved the workload phase that caused it may be over.

The right formulation: predict, for this bank at this moment, whether the next access will want the row currently open. That is a per-bank, per-burst binary prediction, and it is what §1's decision actually needs.

The distinction shows up in what state the two require:

FormulationState per bankResponds to
Hit-rate estimateA counter over many accessesSlow phase changes
Next-access predictionA small saturating counterLocal behaviour

And the counter is not counting hits — it is counting whether keeping the row open paid off. Those differ: a hit means the row was still open and matched; a correct decision to keep it open means a hit occurred and the alternative would have been worse. On a bank where accesses arrive in pairs to the same row and then move on, the hit rate is 50% and keeping the row open is right exactly half the time — which the crossover says is break-even, and a counter measuring the decision's outcome reports directly.

So the predictor's update rule is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   kept the row open, next access HIT       -> the bet paid. Increment.
   kept the row open, next access CONFLICT  -> the bet lost. Decrement.
   closed the row,    next access MISS      -> no information about
                                               whether keeping it would
                                               have hit. See §8.

That third line is the problem §8 is about.

8. The Predictor Cannot Observe Its Own Counterfactual

Here is the structural difficulty, and it is the reason an adaptive page policy is harder than it looks.

When the policy closes a row, it destroys the evidence it needs to evaluate that decision. The next access is a miss. Whether it would have been a hit — in which case closing was wrong — is unobservable, because the row is gone.

So a policy in close mode receives no signal about whether it should switch back. It can stay closed indefinitely on a workload that would have been served better by staying open, and nothing in its inputs contradicts it.

Two mechanisms address this, and both cost something:

Remember the closed row's address. Keep the row address after precharging it, and on the next access compare: if it matches, the decision was wrong. This costs one row-address register per bank and it is exact — the counterfactual becomes observable at the price of storage.

Periodically explore. Occasionally keep a row open despite the counter saying otherwise, and observe the outcome. This costs the occasional bad decision in exchange for information — the same trade Module 21 makes when it retrains a setting that currently works.

§10's block takes the first, because it is exact and the cost is one register per bank against a predictor that is otherwise blind in one of its two states.

9. The Policy State Machine

The per-bank page policy state machine, with six states. The machine starts in idle, where the bank has no open row. When a burst completes it moves to row open, where the row is being held and no decision has yet been made. From there, when the next access to this bank resolves, it moves to predict, which consults that bank's saturating confidence counter. Predict has two exits corresponding to the decision. If the counter favours reuse it moves to hold open, leaving the row available so a same-row access becomes a hit. If the counter favours closing it moves to close now, issuing a precharge so a different-row access becomes a miss rather than a conflict. Both decision states converge on update counter, which observes what the next access actually wanted and adjusts the counter accordingly, incrementing when holding the row paid off and decrementing when it did not. Update counter returns to idle. The two-way exit from predict is the entire policy, and the update state is what makes it adaptive rather than fixed.IDLEROW OPENPREDICTHOLDOPENCLOSENOWUPDATECOUNTERrow activatedrow activatedburst completeburstcompletecounter favours reusecounter favours reusecounter favours closingcounter favours closingcounterfavours…next access observednext access observednext access observednext access observedcounter updatedcounter updated

The two-way exit from PREDICT is the policy, and UPDATE COUNTER is what makes it adaptive. A fixed policy is this machine with one of the two exits removed — which is worth noticing, because it means open-page and close-page are not different designs but degenerate parameterisations of the same one, and §10's block exposes exactly that through its mode parameter.

The edge from CLOSE NOW to UPDATE COUNTER is the one §8 says is hard: the observation available there is a miss, and a miss carries no information about whether holding would have hit — unless the retained row address supplies it.

10. The Page Policy Engine

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// page_policy_engine -- per-bank open / close / adaptive page policy
// with a saturating confidence counter and an observable counterfactual.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - the device, or tRP / tRCD / tWR enforcement. §2: closing after a
//     write must wait tWR, and 13.4 owns whether a precharge may issue.
//     This block requests; the legality gate decides when.
//   - the scheduler's request choice (23.4 owns it)
//   - the hidden-precharge assumption of §1's callout: whether the
//     precharge actually lands in an idle interval is a scheduling
//     outcome this block cannot see.
//
// THE THREE MODES ARE ONE DESIGN. Open-page and close-page are
// degenerate parameterisations (§9), which is why MODE selects rather
// than switching between implementations.
// ---------------------------------------------------------------------
module page_policy_engine #(
  parameter int NUM_BANKS = 16,
  parameter int ROW_W     = 16,
  // 0 = always open, 1 = always close, 2 = adaptive.
  parameter int MODE      = 2,
  // Saturating counter width per bank. §6's callout: hysteresis is
  // mandatory, and 2 bits is the smallest that provides any.
  parameter int CTR_W     = 2,
  // Decide to hold open when the counter is at or above this. With
  // CTR_W = 2 the counter spans 0..3 and a threshold of 2 means two
  // consecutive wins are needed to switch to holding.
  parameter int HOLD_THRESHOLD = 2,
  // Retain the precharged row's address so the counterfactual of §8 is
  // observable. Costs one register per bank and removes the blindness.
  parameter bit TRACK_COUNTERFACTUAL = 1'b1,
  parameter int BK_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
  parameter int CNT_W = 20
) (
  input  logic                clk,
  input  logic                rst_n,

  // ── A burst has completed on this bank: the decision point of §1.
  input  logic                burst_done,
  input  logic [BK_W-1:0]     burst_bank,
  input  logic [ROW_W-1:0]    burst_row,
  // Direction matters for WHEN the precharge may start (§2), not for
  // whether to close. Published onward rather than acted on here.
  input  logic                burst_was_write,

  // ── The next access to a bank, which is the outcome observation.
  input  logic                acc_valid,
  input  logic [BK_W-1:0]     acc_bank,
  input  logic [ROW_W-1:0]    acc_row,

  // ── THE DECISION.
  output logic                close_request,
  output logic [BK_W-1:0]     close_bank,
  // Forwarded so the scheduler applies tWR rather than tRTP (§2).
  output logic                close_after_write,

  // ── Observability: the predictor's own accuracy, which §8 says is
  //    only observable at all because of the retained row address.
  output logic [CNT_W-1:0]    n_held_and_hit,      // the bet paid
  output logic [CNT_W-1:0]    n_held_and_conflict, // the bet lost
  output logic [CNT_W-1:0]    n_closed_and_reused, // closing was WRONG
  output logic [CNT_W-1:0]    n_closed_and_other,  // closing was right
  output logic [CTR_W-1:0]    ctr_of_bank,
  output logic                predict_hold,
  output logic                any_saturated,

  output logic                err_close_unopened,
  output logic                err_mode_invalid
);

  initial begin
    if (NUM_BANKS < 1)
      $fatal(1, "page_policy_engine: NUM_BANKS must be at least 1");
    if (MODE < 0 || MODE > 2)
      $fatal(1, "page_policy_engine: MODE must be 0, 1 or 2");
    if (CTR_W < 2 && MODE == 2)
      // A 1-bit counter has no hysteresis: one observation flips it,
      // which is exactly §6's oscillator.
      $fatal(1, "page_policy_engine: adaptive MODE needs CTR_W >= 2");
    if (HOLD_THRESHOLD < 1 || HOLD_THRESHOLD > (1 << CTR_W) - 1)
      $fatal(1, "page_policy_engine: HOLD_THRESHOLD out of counter range");
  end

  logic [CTR_W-1:0]  ctr      [NUM_BANKS];
  logic              is_open  [NUM_BANKS];
  logic [ROW_W-1:0]  open_row [NUM_BANKS];
  // §8's counterfactual: the row we precharged, kept so a later access
  // can reveal that closing was wrong.
  logic              had_closed [NUM_BANKS];
  logic [ROW_W-1:0]  closed_row [NUM_BANKS];

  // Declared above the assigns that read them.
  logic [CNT_W-1:0] n_hh, n_hc, n_cr, n_co;
  logic             sat, bad_close;

  // ── THE DECISION. Open-page and close-page are the degenerate cases
  //    of the adaptive rule, which is why there is one expression.
  logic hold_now;
  always_comb begin
    unique case (MODE)
      0       : hold_now = 1'b1;    // always open
      1       : hold_now = 1'b0;    // always close
      default : hold_now = burst_done
                         && (ctr[burst_bank] >= CTR_W'(HOLD_THRESHOLD));
    endcase
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int b = 0; b < NUM_BANKS; b++) begin
        // Start at the threshold: neither biased toward holding nor
        // toward closing, so the first few observations decide. Biasing
        // it either way makes the policy's warm-up a hidden default.
        ctr[b]        <= CTR_W'(HOLD_THRESHOLD);
        is_open[b]    <= 1'b0;
        open_row[b]   <= '0;
        had_closed[b] <= 1'b0;
        closed_row[b] <= '0;
      end
      n_hh <= '0; n_hc <= '0; n_cr <= '0; n_co <= '0;
      sat <= 1'b0; bad_close <= 1'b0;
    end else begin
      bad_close <= 1'b0;

      // ── The outcome observation, BEFORE the new decision, so an
      //    access and a burst completion in the same cycle are ordered
      //    correctly: the access reports on the PREVIOUS decision.
      if (acc_valid) begin
        if (is_open[acc_bank]) begin
          // The row was held. Did the bet pay?
          if (open_row[acc_bank] == acc_row) begin
            if (n_hh != {CNT_W{1'b1}}) n_hh <= n_hh + CNT_W'(1); else sat <= 1'b1;
            if (ctr[acc_bank] != {CTR_W{1'b1}})
              ctr[acc_bank] <= ctr[acc_bank] + CTR_W'(1);
          end else begin
            if (n_hc != {CNT_W{1'b1}}) n_hc <= n_hc + CNT_W'(1); else sat <= 1'b1;
            if (ctr[acc_bank] != '0)
              ctr[acc_bank] <= ctr[acc_bank] - CTR_W'(1);
          end
        end else if (TRACK_COUNTERFACTUAL && had_closed[acc_bank]) begin
          // §8: the row was closed. The retained address reveals
          // whether closing was wrong -- the ONLY way this state
          // produces a learning signal at all.
          if (closed_row[acc_bank] == acc_row) begin
            // Closing was WRONG: this would have been a hit.
            if (n_cr != {CNT_W{1'b1}}) n_cr <= n_cr + CNT_W'(1); else sat <= 1'b1;
            if (ctr[acc_bank] != {CTR_W{1'b1}})
              ctr[acc_bank] <= ctr[acc_bank] + CTR_W'(1);
          end else begin
            if (n_co != {CNT_W{1'b1}}) n_co <= n_co + CNT_W'(1); else sat <= 1'b1;
            if (ctr[acc_bank] != '0)
              ctr[acc_bank] <= ctr[acc_bank] - CTR_W'(1);
          end
        end
        // The access opens its row either way.
        is_open[acc_bank]    <= 1'b1;
        open_row[acc_bank]   <= acc_row;
        had_closed[acc_bank] <= 1'b0;
      end

      // ── The decision, at the burst-completion point of §1.
      if (burst_done) begin
        if (!is_open[burst_bank] && !acc_valid) bad_close <= 1'b1;
        if (!hold_now) begin
          is_open[burst_bank]    <= 1'b0;
          had_closed[burst_bank] <= 1'b1;
          closed_row[burst_bank] <= burst_row;
        end
      end
    end
  end

  assign close_request     = burst_done && !hold_now;
  assign close_bank        = burst_bank;
  assign close_after_write = burst_done && !hold_now && burst_was_write;

  assign n_held_and_hit      = n_hh;
  assign n_held_and_conflict = n_hc;
  assign n_closed_and_reused = n_cr;
  assign n_closed_and_other  = n_co;
  assign ctr_of_bank         = ctr[acc_valid ? acc_bank : burst_bank];
  assign predict_hold        = hold_now;
  assign any_saturated       = sat;
  assign err_close_unopened  = bad_close;
  assign err_mode_invalid    = 1'b0;   // MODE is checked at elaboration

endmodule

Three decisions in that block are the chapter rather than the implementation.

The counter initialises at the threshold. Starting it at zero biases the policy toward closing during warm-up; starting it at maximum biases toward holding. Starting at the threshold means the first observations decide, and the policy has no hidden default — which matters because a short-lived workload may never leave its warm-up.

The outcome observation is ordered before the decision. An access and a burst completion can occur in the same cycle, and the access is reporting on the previous decision while the completion is making the next one. Ordering them the other way attributes an outcome to the decision that had not been made yet.

n_closed_and_reused is the output that makes §8 tractable. It counts the times closing was wrong — an access arrived wanting the row that had just been precharged — and it is only observable because closed_row was retained. Without it the predictor learns in one direction only, and a policy stuck in close mode has no input that could ever move it.

11. The Counterfactual Accountant

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// policy_counterfactual -- what would open-page, close-page and a
// perfect predictor each have cost on THIS access stream?
//
// CLASSIFICATION: educational, synthesisable.
//
// WHY IT EXISTS: §4 shows the winner depends on the workload's hit
// probability, and §3 gives the crossover in closed form. A design can
// therefore decide which policy to use IF it knows h -- and this block
// measures the three costs directly instead, which is stronger because
// it needs no distributional assumption at all.
//
// WHAT IT DOES NOT MODEL:
//   - the device, the deployed policy, or the scheduler
//   - the hidden-precharge assumption (§1's callout). Close-page's
//     cost here assumes the precharge lands in an idle interval; if it
//     does not, the real cost is higher and this understates it.
// ---------------------------------------------------------------------
module policy_counterfactual #(
  parameter int NUM_BANKS = 16,
  parameter int ROW_W     = 16,
  // VERIFIED weights from 23.3 §2 / 23.1 §2.
  parameter int T_RCD = 11,
  parameter int T_RP  = 11,
  parameter int BK_W  = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
  parameter int ACC_W = 32
) (
  input  logic                clk,
  input  logic                rst_n,

  input  logic                acc_valid,
  input  logic [BK_W-1:0]     acc_bank,
  input  logic [ROW_W-1:0]    acc_row,
  // The NEXT access's row for this bank, if known. A perfect predictor
  // needs it, which is exactly why a perfect predictor is not
  // implementable -- this input exists only to compute the CEILING.
  input  logic                lookahead_valid,
  input  logic [ROW_W-1:0]    lookahead_row,

  // ── The three costs, in cycles, on the observed stream.
  output logic [ACC_W-1:0]    cost_open,
  output logic [ACC_W-1:0]    cost_close,
  output logic [ACC_W-1:0]    cost_perfect,
  output logic [ACC_W-1:0]    n_accesses,

  // ── The crossover, evaluated: is the stream's behaviour above or
  //    below tRCD/(tRP+tRCD)? Reported as the comparison rather than
  //    as a ratio, so nothing divides.
  output logic                open_is_better,
  output logic                counts_valid,

  output logic                err_weights_invalid
);

  initial begin
    if (T_RCD < 1 || T_RP < 1)
      $fatal(1, "policy_counterfactual: weights must be positive");
    if (ACC_W < 16)
      $fatal(1, "policy_counterfactual: ACC_W too small to accumulate");
  end

  logic [ROW_W-1:0] open_row [NUM_BANKS];
  logic             valid_r  [NUM_BANKS];

  logic [ACC_W-1:0] c_open, c_close, c_perf, n_acc;
  logic             sat;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int b = 0; b < NUM_BANKS; b++) begin
        open_row[b] <= '0;
        valid_r[b]  <= 1'b0;
      end
      c_open <= '0; c_close <= '0; c_perf <= '0; n_acc <= '0; sat <= 1'b0;
    end else if (acc_valid) begin
      if (n_acc == {ACC_W{1'b1}}) begin
        sat <= 1'b1;
      end else begin
        n_acc <= n_acc + ACC_W'(1);

        // ── OPEN-PAGE: hit if the row matches, else a conflict.
        if (!valid_r[acc_bank])
          c_open <= c_open + ACC_W'(T_RCD);                 // first touch
        else if (open_row[acc_bank] == acc_row)
          c_open <= c_open;                                  // hit: zero
        else
          c_open <= c_open + ACC_W'(T_RP + T_RCD);           // conflict

        // ── CLOSE-PAGE: always a miss. Constant, independent of the
        //    stream -- which is §3's whole point, in one line.
        c_close <= c_close + ACC_W'(T_RCD);

        // ── PERFECT: a hit costs nothing; anything else costs a miss,
        //    because the row was closed in advance. §5's ceiling.
        if (valid_r[acc_bank] && (open_row[acc_bank] == acc_row))
          c_perf <= c_perf;
        else
          c_perf <= c_perf + ACC_W'(T_RCD);

        open_row[acc_bank] <= acc_row;
        valid_r[acc_bank]  <= 1'b1;
      end
    end
  end

  assign cost_open    = c_open;
  assign cost_close   = c_close;
  assign cost_perfect = c_perf;
  assign n_accesses   = n_acc;
  // The crossover test, as a comparison rather than a ratio.
  assign open_is_better = (c_open < c_close);
  assign counts_valid   = !sat && (n_acc != '0);
  assign err_weights_invalid = 1'b0;   // checked at elaboration

endmodule

This block is the most directly useful thing in the chapter, and the reason is in its header. §3 gives the crossover in closed form, so a design that knows its hit probability can choose a policy analytically. A design that does not know it can measure all three costs instead — and that is stronger, because it needs no assumption about the workload's distribution at all.

The cost_perfect output is the ceiling of §5, and it is computed from a lookahead input that no real controller has. That is deliberate: the block computes an upper bound on what adaptation could buy, so a design can see whether the achievable gain justifies the predictor before building one.

The precharge timing gate

§10's engine requests a close and explicitly defers when it may happen. §2 established that the delay differs by direction — tRTP = 6 cycles after a read, tWR = 12 cycles after a write, both VERIFIED — so something has to apply the right one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// precharge_timing_gate -- applies tRTP after a read and tWR after a
// write before a requested precharge becomes eligible.
//
// CLASSIFICATION: educational, synthesisable. Supplies the timing
// page_policy_engine requests and deliberately does not own.
//
// WHAT IT DOES NOT MODEL:
//   - the device, or command-bus arbitration
//   - LEGALITY (13.4) or the commit point (17.1). Eligible is not
//     issued, and this block never claims otherwise.
//   - whether the precharge lands in an idle interval, which is §1's
//     callout's assumption and a scheduling outcome.
//
// §2: the two delays differ by 2x, so a gate applying one value to
// both would either violate tWR after writes or needlessly delay
// precharges after reads.
// ---------------------------------------------------------------------
module precharge_timing_gate #(
  parameter int NUM_BANKS = 16,
  // VERIFIED at DDR4-1600K: tRTP = 6 nCK, tWR = 15 ns = 12 nCK.
  parameter int T_RTP = 6,
  parameter int T_WR  = 12,
  parameter int BK_W  = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
  parameter int D_W   = (T_WR > T_RTP)
                          ? ((T_WR   <= 1) ? 1 : $clog2(T_WR + 1))
                          : ((T_RTP  <= 1) ? 1 : $clog2(T_RTP + 1)),
  parameter int OCC_W = $clog2(NUM_BANKS + 1)
) (
  input  logic                 clk,
  input  logic                 rst_n,

  // -- A close request from page_policy_engine, with its direction.
  input  logic                 close_request,
  input  logic [BK_W-1:0]      close_bank,
  input  logic                 close_after_write,

  // -- Eligibility, per bank. NOT an issue signal.
  output logic [NUM_BANKS-1:0] pre_eligible,
  output logic [D_W-1:0]       wait_of_bank,
  output logic [OCC_W-1:0]     n_waiting,

  // -- Observability: cycles spent waiting, split by direction, so the
  //    §2 asymmetry's real cost on this workload is visible.
  output logic [23:0]          cy_waiting_read,
  output logic [23:0]          cy_waiting_write,
  output logic                 any_saturated,

  output logic                 err_request_while_waiting
);

  initial begin
    if (NUM_BANKS < 1)
      $fatal(1, "precharge_timing_gate: NUM_BANKS must be at least 1");
    if (T_RTP < 1 || T_WR < 1)
      $fatal(1, "precharge_timing_gate: delays must be positive");
    if (T_WR < T_RTP)
      // §2: write recovery is the LONGER of the two. A parameterisation
      // claiming otherwise inverts the asymmetry the block exists for.
      $fatal(1, "precharge_timing_gate: T_WR (%0d) below T_RTP (%0d)",
             T_WR, T_RTP);
  end

  logic [D_W-1:0] cnt    [NUM_BANKS];
  logic           pend   [NUM_BANKS];
  logic           was_wr [NUM_BANKS];

  // Declared above the assigns that read them.
  logic [23:0] cy_rd, cy_wr;
  logic        sat, bad_req;

  logic [OCC_W-1:0] n_wait;
  always_comb begin
    n_wait = '0;
    for (int b = 0; b < NUM_BANKS; b++)
      if (pend[b] && (cnt[b] != '0)) n_wait = n_wait + OCC_W'(1);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int b = 0; b < NUM_BANKS; b++) begin
        cnt[b]    <= '0;
        pend[b]   <= 1'b0;
        was_wr[b] <= 1'b0;
      end
      cy_rd <= '0; cy_wr <= '0; sat <= 1'b0; bad_req <= 1'b0;
    end else begin
      bad_req <= 1'b0;

      // -- Age every pending bank, charging the wait to the right
      //    direction so §2's asymmetry is measurable rather than
      //    merely asserted.
      for (int b = 0; b < NUM_BANKS; b++) begin
        if (pend[b] && (cnt[b] != '0)) begin
          cnt[b] <= cnt[b] - D_W'(1);
          if (was_wr[b]) begin
            if (cy_wr == 24'hFFFFFF) sat <= 1'b1; else cy_wr <= cy_wr + 24'd1;
          end else begin
            if (cy_rd == 24'hFFFFFF) sat <= 1'b1; else cy_rd <= cy_rd + 24'd1;
          end
        end
      end

      // -- A new request loads the direction-appropriate delay.
      if (close_request) begin
        if (pend[close_bank] && (cnt[close_bank] != '0)) bad_req <= 1'b1;
        pend[close_bank]   <= 1'b1;
        was_wr[close_bank] <= close_after_write;
        cnt[close_bank]    <= close_after_write ? D_W'(T_WR) : D_W'(T_RTP);
      end
    end
  end

  always_comb
    for (int b = 0; b < NUM_BANKS; b++)
      pre_eligible[b] = pend[b] && (cnt[b] == '0);

  assign wait_of_bank     = cnt[close_bank];
  assign n_waiting        = n_wait;
  assign cy_waiting_read  = cy_rd;
  assign cy_waiting_write = cy_wr;
  assign any_saturated    = sat;
  assign err_request_while_waiting = bad_req;

endmodule

Two assertions belong with it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P11 -- the loaded delay matches the direction. §2's asymmetry, and
// the check that a single value is not being applied to both.
property p_delay_matches_direction;
  @(posedge clk) disable iff (!rst_n)
    close_request
      |=> (wait_of_bank == ($past(close_after_write, 1)
                              ? D_W'(T_WR) : D_W'(T_RTP)));
endproperty
assert property (p_delay_matches_direction);

// P12 -- eligibility requires the counter to have expired. A gate that
// asserted early would violate tWR, which is a device rule and not a
// policy preference.
property p_eligible_only_when_expired;
  @(posedge clk) disable iff (!rst_n)
    pre_eligible[close_bank] |-> (wait_of_bank == '0);
endproperty
assert property (p_eligible_only_when_expired);

// -- Cover: a write-direction wait, which a read-only trace never
// exercises and which is the longer of the two.
cover property (@(posedge clk) disable iff (!rst_n)
                  close_request && close_after_write);
// Several banks waiting at once -- the multi-bank case.
cover property (@(posedge clk) disable iff (!rst_n) n_waiting > OCC_W'(1));

The two cycle counters split by direction on purpose. §2 claims the write case is twice as slow to start; cy_waiting_write against cy_waiting_read measures it on the actual workload, which is what turns §2 from a parameter table into a figure a design can act on. A write-heavy workload shows the write counter dominating, and §15's eighth row is the diagnostic that follows.

12. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: P1-P7 reference page_policy_engine's internals,
// P8-P10 policy_counterfactual's; both are written as though bound
// into their module, so clk and rst_n are visible directly.

// P1 -- MODE 0 never closes. Open-page as a degenerate case (§9), and
// the check that the mode parameter is real rather than decorative.
property p_open_mode_never_closes;
  @(posedge clk) disable iff (!rst_n)
    (MODE == 0) |-> !close_request;
endproperty
assert property (p_open_mode_never_closes);

// P2 -- MODE 1 closes on every burst completion. Close-page, likewise.
property p_close_mode_always_closes;
  @(posedge clk) disable iff (!rst_n)
    ((MODE == 1) && burst_done) |-> close_request;
endproperty
assert property (p_close_mode_always_closes);

// P3 -- a close is requested only at a burst completion. §1: the
// decision has one moment, and closing at any other point would
// precharge a row that may still be carrying a transfer.
property p_close_only_at_burst_end;
  @(posedge clk) disable iff (!rst_n)
    close_request |-> burst_done;
endproperty
assert property (p_close_only_at_burst_end);

// P4 -- the adaptive decision follows the counter and the threshold.
property p_adaptive_follows_counter;
  @(posedge clk) disable iff (!rst_n)
    ((MODE == 2) && burst_done)
      |-> (predict_hold == (ctr[burst_bank] >= CTR_W'(HOLD_THRESHOLD)));
endproperty
assert property (p_adaptive_follows_counter);

// P5 -- a held row that hit increments; a held row that conflicted
// decrements. The update rule of §7, checkable.
property p_counter_update_direction;
  @(posedge clk) disable iff (!rst_n)
    (acc_valid && is_open[acc_bank] && (open_row[acc_bank] == acc_row)
       && (ctr[acc_bank] != {CTR_W{1'b1}}))
      |=> (ctr[$past(acc_bank, 1)] == $past(ctr[acc_bank], 1) + 1);
endproperty
assert property (p_counter_update_direction);

// P6 -- THE hysteresis property of §6's callout. A single observation
// cannot move the counter across the threshold when it starts more
// than one step away. Without this the policy is an oscillator.
property p_single_observation_cannot_flip;
  @(posedge clk) disable iff (!rst_n)
    ((MODE == 2) && acc_valid
       && (ctr[acc_bank] >= CTR_W'(HOLD_THRESHOLD) + CTR_W'(1)))
      |=> (ctr[$past(acc_bank, 1)] >= CTR_W'(HOLD_THRESHOLD));
endproperty
assert property (p_single_observation_cannot_flip);

// P7 -- the counterfactual counter only moves when tracking is
// enabled. §8: without it the predictor is blind in close mode, and
// that blindness must be visible rather than silently absent.
property p_counterfactual_requires_tracking;
  @(posedge clk) disable iff (!rst_n)
    (n_closed_and_reused != $past(n_closed_and_reused, 1))
      |-> (TRACK_COUNTERFACTUAL == 1'b1);
endproperty
assert property (p_counterfactual_requires_tracking);

// P8 -- close-page's cost is exactly tRCD per access. §3's "independent
// of h", as an invariant rather than an observation.
property p_close_cost_is_linear;
  @(posedge clk) disable iff (!rst_n)
    counts_valid |-> (cost_close == n_accesses * ACC_W'(T_RCD));
endproperty
assert property (p_close_cost_is_linear);

// P9 -- a perfect predictor is never worse than either fixed policy.
// §5's ceiling, and the check that the ceiling is computed correctly.
property p_perfect_bounds_both;
  @(posedge clk) disable iff (!rst_n)
    counts_valid |-> ((cost_perfect <= cost_open)
                   && (cost_perfect <= cost_close));
endproperty
assert property (p_perfect_bounds_both);

// P10 -- open-page's cost never exceeds (tRP+tRCD) per access, and
// close-page's never falls below tRCD. The two bounds of §4's table.
property p_costs_within_bounds;
  @(posedge clk) disable iff (!rst_n)
    counts_valid |-> (cost_open <= n_accesses * ACC_W'(T_RP + T_RCD));
endproperty
assert property (p_costs_within_bounds);

// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) close_request);
cover property (@(posedge clk) disable iff (!rst_n)
                  burst_done && predict_hold && (MODE == 2));
// The counterfactual fired: closing was WRONG and the predictor learned
// it. §8's mechanism actually exercised -- without this cover, the
// retained row address is untested.
cover property (@(posedge clk) disable iff (!rst_n)
                  n_closed_and_reused != $past(n_closed_and_reused, 1));
// The counter saturated at each end -- the hysteresis limits.
cover property (@(posedge clk) disable iff (!rst_n)
                  (MODE == 2) && acc_valid && (ctr[acc_bank] == '0));
cover property (@(posedge clk) disable iff (!rst_n)
                  (MODE == 2) && acc_valid && (ctr[acc_bank] == {CTR_W{1'b1}}));
// Open-page better, and close-page better, on the same instance across
// two runs -- §4's flip actually observed.
cover property (@(posedge clk) disable iff (!rst_n)
                  counts_valid && open_is_better);
cover property (@(posedge clk) disable iff (!rst_n)
                  counts_valid && !open_is_better);
// A close after a write: §2's tWR path, which a read-only trace misses.
cover property (@(posedge clk) disable iff (!rst_n) close_after_write);

P6 is the property this chapter exists to make possible, and it is the one that distinguishes a predictor from §6's oscillator. Without hysteresis a single observation flips the decision, and §6's table showed that an anti-correlated predictor is 5.5× worse than open-page and 11× worse than a perfect one.

P9 is worth having as a self-check on the counterfactual block: a perfect predictor must bound both fixed policies, and if it does not, the ceiling is being computed wrongly and every conclusion drawn from it is suspect.

13. Corner Cases

CaseBehaviourWhy
MODE = 0Never closes; close_request flat lowOpen-page as a degenerate case — P1
MODE = 1Closes on every burstClose-page — P2
MODE = 2, CTR_W = 1$fatal at elaborationOne bit has no hysteresis — §6's oscillator
Counter at the threshold on resetFirst observations decide; no hidden default§10 — biasing warm-up either way is a silent policy
Access and burst completion same cycleOutcome observed first, then the decisionThe access reports on the previous decision
Close after a writeclose_after_write set; tWR applies§2 — twice the delay of a read
TRACK_COUNTERFACTUAL = 0n_closed_and_reused stays zeroP7; the predictor is blind in close mode and it shows
Bank never accessedCounter stays at its initial valueNo observations, no adaptation
Alternating two rows in one bankCounter oscillates within hysteresis§6's pattern; the counter should settle low
Workload exactly at the crossoverBoth fixed costs equal; adaptation gains nothing§4's tie row; the predictor tracks noise
Every access to a fresh bankOpen-page charged tRCD, not a conflictA first touch has no open row to conflict with
Counter saturated high, then a phase changeTakes CTR_W observations to respondThe cost of hysteresis, and it is the point
cost_perfect exceeding either fixed costImpossible — P9The ceiling is mis-computed if it happens

Row twelve is the honest cost of §6's fix. Hysteresis makes the predictor slow to respond to a genuine phase change — it takes as many observations to cross the threshold as the counter is wide. That is a real cost, deliberately paid, and it is why CTR_W is a parameter: two bits is the minimum that works and more is not obviously better.

14. DV — Replay the Crossover

The checker reproduces §3's algebra, §4's table and §6's thrash from the RTL, using a reference that computes costs by closed form rather than by accumulation.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent policy cost reference. Computes each policy's cost from
// CLASS COUNTS by closed form, rather than by per-access accumulation
// -- a different method, so agreement is evidence.
// SIMULATION-ONLY.
class policy_ref;
  int unsigned t_rcd, t_rp;
  int unsigned n_hit, n_other;     // over the observed stream

  function int unsigned cost_open();
    return n_other * (t_rp + t_rcd);
  endfunction
  function int unsigned cost_close();
    return (n_hit + n_other) * t_rcd;
  endfunction
  function int unsigned cost_perfect();
    return n_other * t_rcd;
  endfunction
  // §3's crossover, as a comparison with no division.
  function bit open_better();
    return cost_open() < cost_close();
  endfunction
  // The crossover hit rate, as a numerator/denominator pair.
  function void crossover(output int unsigned num, output int unsigned den);
    num = t_rcd; den = t_rp + t_rcd;
  endfunction
endclass
CheckWhat it establishes
All three costs match the RTL over a 1000-access streamClosed form agrees with accumulation
h = 90%: expect 2200 / 11000 / 1100§4's and §5's published rows
h = 50%: expect 11000 / 11000, and open_is_better low§4's exact tie, and the boundary's direction
h = 10%: expect 19800 / 11000§4's low-locality row
Sweep h from 0 to 100% in 5% steps; find where open_is_better flips§3's crossover located empirically at 50%
Set t_rp = 22, t_rcd = 11; expect the crossover at 1/3The general form, not the bin-specific value
Anti-correlated predictor on h = 90%: expect 12100§6's thrash, from the RTL
CTR_W = 2, alternating rows: counter settles low, no oscillationP6 doing its job
TRACK_COUNTERFACTUAL = 0 on a close-mode workload: counter never rises§8's blindness, demonstrated
cost_perfect never exceeds either fixed cost, over 10,000 streamsP9 as a bound

The sixth and ninth checks are the two worth running, and the ninth produces the chapter's closing failure report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  THE PREDICTOR THAT CANNOT LEARN ITS WAY OUT
    MODE = 2, CTR_W = 2, HOLD_THRESHOLD = 2, counter starts at 2.
    TRACK_COUNTERFACTUAL = 0.

    workload: h = 90% -- open-page would cost 2200 cy per 1000 accesses.
    but the stream BEGINS with a burst of 3 different-row accesses.

    access 1-3 : row held, conflict each time -> counter 2,1,0
    access 4   : counter 0 < threshold 2      -> CLOSE
    access 5+  : every access is a MISS. The row is closed before each
                 one, so the access never reveals that it WANTED the
                 row that was just precharged.
                 with TRACK_COUNTERFACTUAL = 0 there is no observation
                 at all -> the counter NEVER MOVES.

    outcome over 1000 accesses:
      actual cost      : 3 conflicts x 22 + 997 misses x 11 = 11033 cy
      open-page would  : 2200 cy
      close-page would : 11000 cy
      perfect would    : 1100 cy

    the adaptive policy performed WORSE THAN BOTH fixed policies
    (11033 > 11000 > 2200) on a workload with 90% locality.

    diagnosis : three unrepresentative accesses drove the counter to
      zero, and §8's blindness meant no subsequent evidence could ever
      contradict it. The policy is stuck, correctly by its own rules.
    what still passes : P1-P6 all hold. P4 holds -- the decision
      follows the counter. P6 holds -- no single observation flipped
      anything. The engine is working exactly as specified.
    caught by : this check, and by n_closed_and_reused being
      permanently zero -- which P7 explains is expected when tracking
      is disabled, and which is therefore the signature rather than
      the bug.
    the fix : TRACK_COUNTERFACTUAL = 1. With it, access 5 reveals that
      the precharged row was wanted, the counter rises, and the policy
      returns to holding within CTR_W observations.

That is the right note for the module's verification to end on. Every property holds, the engine is correct by its specification, and the policy is 5× worse than the fixed alternative it replaced — because a structural blindness in one of its two states meant three early observations became permanent. §8 named the blindness; this measures what it costs.

15. Debugging

SymptomLikely causeHow to confirm
Adaptive worse than both fixed policiesPredictor stuck — §14n_closed_and_reused permanently zero; enable tracking
Adaptive gains nothingWorkload near the 50% crossover — §4The counterfactual costs will be nearly equal
Policy oscillates every accessNo hysteresis, or CTR_W = 1P6; §6's callout
Slow to respond to a phase changeHysteresis, working as designed — §13Expected; CTR_W observations to cross
Open-page chosen and throughput poorHit probability below the crossoverRun the counterfactual; §11 needs no assumption
Close-page chosen and latency poorEvery access pays tRCD — §1Above 50% hit probability, open-page halves it
Close-page's predicted benefit not realisedPrecharge not landing in an idle interval§1's callout; §11's header states the assumption
Close-page worse than expected on writestWR = 12 against tRTP = 6 — §2Write-heavy workloads narrow the hiding window
Counter pinned at one endA sustained phase, or unrepresentative warm-up§14's report; check the first observations
err_close_unopenedA close requested for a bank with no open rowScheduler and policy disagree on bank state
Crossover appears to be other than 50%Correct — it is tRCD/(tRP+tRCD)§3's callout; 50% is bin-specific
Adaptive better than cost_perfectImpossible — P9The ceiling is mis-computed

Row seven is the one most likely to be reported as a policy failure when it is a scheduling interaction. Close-page's entire benefit rests on the precharge happening while nothing waits for it (§1's callout), and if the command bus is busy or the next request to that bank arrives early, the precharge lands on the critical path after all — at which point close-page has paid tRCD and gained nothing.

Row eleven is worth expecting rather than investigating. The crossover is tRCD / (tRP + tRCD), and any bin where those differ produces a different threshold.

16. Misconceptions

“A row stays open because that is how DRAM works.” §1. It stays open because a controller chose not to close it. Closing is a policy with a cost model.

“Close-page eliminates the precharge.” §1's callout. It moves it out of a waiting request's latency into an otherwise idle interval. The saving is exactly tRP, and only if that interval really was idle.

“Open-page is the better default.” §4. It costs 8800 cycles more than close-page on a 10%-locality workload over 1000 accesses. It is the higher-variance policy, spanning a factor of 9 across the workload range.

“The crossover must be measured.” §3. It is tRCD / (tRP + tRCD), derivable from two published numbers, and it depends only on the device.

“The crossover is 50%.” §3's callout. It is 50% at this bin, where tRP = tRCD. Quoting the value without the expression is quoting an artefact.

“Adaptation can only help.” §6. An anti-correlated predictor is 5.5× worse than open-page and 11× worse than a perfect one, because it combines both policies' failure modes.

“Hysteresis is a refinement.” §6's callout. Without it, an alternating access pattern makes the predictor systematically wrong rather than merely noisy.

“The predictor should track the bank's hit rate.” §7. It should predict whether the next access wants the row currently open, which is a different and more local question.

“A page predictor is a branch predictor.” §8's callout. The structure is the same; the difference is that closing a row destroys the evidence needed to evaluate closing it, which a branch never does.

“More counter bits make a better predictor.” §13's row twelve. More bits mean more hysteresis, which means slower response to real phase changes. Two is the minimum that works and more is a trade.

“Closing after a write is the same as after a read.” §2. tWR is 12 cycles against tRTP's 6, so the precharge starts twice as late and the hiding window is half as wide.

“If the assertions pass, the policy is right.” §12's callout and §14's report. Every property held while the policy performed 5× worse than the fixed alternative it replaced.

17. Interview Reasoning

What is the page-policy decision, and when is it made? At the cycle a burst completes: close the row or leave it open. Leaving it open bets the next access to that bank wants the same row.

Give the two policies' costs. Open-page: 0 on a same-row access, tRP + tRCD on a different-row one. Close-page: tRCD always, because the row was already closed.

Derive the crossover. Open-page's expected cost is (1-h)(tRP + tRCD) and close-page's is tRCD. Setting them equal gives h = tRCD / (tRP + tRCD) — 50% where tRP = tRCD, and it depends only on the device.

Does close-page eliminate the precharge? No, it moves it. The precharge happens when the previous burst ends rather than when a request is waiting, so the saving is exactly tRP — and only if that interval was genuinely idle.

What is the ceiling on adaptation? A perfect predictor converts every conflict into a miss, so it beats open-page by the conflict-to-miss ratio, (tRP + tRCD)/tRCD — a factor of 2 at this bin, on the non-hit accesses only.

Can an adaptive policy be worse than both fixed ones? Yes, and badly. An anti-correlated predictor pays close-page's premium where open-page was free and open-page's penalty where close-page was cheap — 5.5× worse than open-page on a 90%-locality workload.

Why is hysteresis mandatory? Because a memoryless predictor on an alternating access pattern is systematically wrong: the outcome it just observed is precisely the wrong guide to the next one.

What should the predictor predict? Not the bank's hit rate — whether the next access to this bank will want the row currently open. That is local, per-burst, and it is what the decision needs.

What is structurally hard about learning it? Closing the row destroys the evidence. In close mode every access is a miss, and whether it would have hit is unobservable unless the precharged row's address was retained.

Your adaptive policy is worse than close-page on a high-locality workload. First hypothesis? That it is stuck — an unrepresentative warm-up drove the counter to the closing side, and without counterfactual tracking no later evidence can move it back.

18. Exercises

  1. Recompute §4's five-row table for a device with tRP = 22 and tRCD = 11. Where is the crossover, which policy benefits, and what is the intuition for the direction of the shift?

  2. Derive the general crossover expression and evaluate it for tRP/tRCD ratios of 1/2, 1, and 2. State the limit as tRP grows without bound and explain it in one sentence.

  3. §5 gives adaptation's ceiling as the conflict-to-miss ratio on non-hit accesses. Derive the ceiling as a function of h and show it vanishes as h approaches 1. What does that say about when a predictor is worth building?

  4. Construct the access pattern that makes a CTR_W = 2 predictor maximally wrong, and compute its cost against both fixed policies. Then find the smallest CTR_W that defeats your pattern, and the pattern that defeats that.

  5. §14's report has the predictor stuck after three unrepresentative accesses. Compute how many counterfactual observations are needed to recover with CTR_W = 2 and HOLD_THRESHOLD = 2, and design a warm-up rule that prevents the situation without biasing the policy.

  6. §2 notes tWR is twice tRTP. Model a workload that is 50% writes and recompute §4's table, stating how you account for the difference in when the precharge may start. Which policy's figure changes?

  7. §11's counterfactual block needs a lookahead input for cost_perfect. Argue whether a real controller could approximate it from its own queue, and what the approximation would be worth given 23.4 §5's reordering.

  8. The counterfactual block assumes close-page's precharge is hidden. Modify it to take a hidden fraction parameter and recompute §4's crossover for fractions of 1.0, 0.5 and 0.0. At which fraction does close-page stop winning anywhere?

19. Module 23, Complete

Five chapters, one question: where does the performance go, and which decisions get it back?

Latency is a decomposition, and one component is unbounded. Chapter 23.1 split a request into six disjoint, exhaustive components that sum exactly, and found that 26 of 33 cycles on a row miss are device timing no controller can reduce. It also found that queue wait has no upper bound in any specification — so all the variance in a latency distribution comes from one component, the mean is the wrong statistic for a right-skewed distribution, and the maximum is nearly free to measure and the single highest-value addition.

Peak is attainable, which changes the question. 23.2 derived 12.8 GB/s and then simulated four workload models spanning 10.3% to exactly 100% of it — so the gap is a property of the workload rather than of DDR. It built measure D, which 12.4 named and did not construct: every cycle charged to exactly one named cause under a strict priority order, with exhaustiveness asserted. And it established that independent fractional costs multiply rather than add, and that a priority-ordered attribution undercounts every lower-priority cause where a higher one coincides — a projection, not a partition of causes.

Locality has a cause, a structure, and a 127× range. 23.3 showed that class counts are a function of the sequence plus the mapping plus the bank state, that two mappings can produce identical counts with opposite temporal structure, and that the pathological property is gcd(stride, blocks-per-row) rather than stride magnitude. It also found the largest single effect in the module: two streams each with perfect locality cost 127.5× more when they collide in one bank — and the collapse is a cliff at two streams, not a gradient.

Reordering is the largest lever, and it needs a bound. 23.4 built FR-FCFS from its published rules and measured both numbers: 87.5% of row work recovered, and a request displaced without bound. A bypass cap of 4 retains 85.7% of the gain with displacement bounded provably — and a regression cannot discover a bound that does not exist, which is why the guarantee has to be an invariant over state rather than an observation over a trace.

And the last decision is algebra. This chapter found that the open-versus-close crossover is tRCD / (tRP + tRCD) — derivable from two published numbers, dependent only on the device — that neither fixed policy wins across workloads, that a perfect predictor's ceiling is the conflict-to-miss ratio, and that a wrong predictor is worse than both fixed policies because it combines their failure modes.

The discipline every chapter repeated, and the one that made a quantitative module possible at all: every number derived from a stated model with verified inputs, and every model's assumptions published alongside its result. Modules 19 to 22 refused to compute what they could not observe. This module had to compute, so the honesty moved from whether to how — three categories on every figure, sum checks on every decomposition, a denominator named for every ratio, and a simulation run rather than an expectation asserted. That last discipline earned its place: the simulations corrected me twice, at 23.3 §3 where two mappings I expected to differ turned out identical, and at 23.3 §8 where the multi-stream collapse proved to be a cliff rather than the gradient I assumed.

And the limit every chapter restated: a counter cannot separate irreducible from avoidable. 23.2 §8 put it structurally — a cycle charged to tCCD_L was genuinely unavailable, and whether the workload should have been in that bank group is a question no counter can reach. Every measurement in five chapters says where the cycles went. None of them says who should have done otherwise, and each chapter's closing callout marks exactly where that boundary falls.

Module 24 turns from performance to power and mobility: what changes when the same architecture has to run on a battery, why LPDDR is a different device rather than a tuned one, and how low-power states interact with everything this module just quantified.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

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

Where this fits

Part of the DDR curriculum.