Skip to content
VLSI Mentor

DDR · Module 5

Bank Groups

Not all bank pairs are equally independent. A bank group is the scope at which the internal column data path is shared, and the three-way classification of a request against its predecessor is the interface every later timing module consumes.

Chapter 5.2 established that a bank owns its row state and shares everything downstream of it. It also flagged its own most important simplification: §5's RTL treats every bank as equally independent, which is not true of any DDR4 or DDR5 device.

This chapter corrects that, and the correction begins with a question worth sitting with:

If banks already provide independent row state, why would a device need another hierarchical level at all?

The answer is not "because there are a lot of banks and they needed organising." It is that one resource a bank needs was never replicated per bank, and the level exists to make the scope of that sharing visible to the controller.

1. The Resource That Was Never Replicated

Chapter 5.2 §3 sorted a bank's resources by sharing scope and named the middle one without developing it. Develop it now.

Serving a column access requires more than an open row. The row sits in the sense amplifiers; getting the selected bits out of there and toward the interface requires internal circuitry — column selection, and the data path that carries the result toward the device's I/O. That machinery is not replicated per bank.

And it is not replicated per bank for the reason everything in DRAM is not replicated: area. Chapter 1.6 established that DRAM economics are dominated by bits per unit area, and Chapter 5.2 §3 noted that a bank already costs a full row's worth of sense amplifiers. Giving every bank its own complete column data path as well would be a large additional cost for a resource that is busy only briefly during each access.

So it is shared — by a set of banks rather than by all of them. That set is a bank group.

Bank group zero contains four banks that share column path zero. Bank group one contains four banks that share column path one. Both column paths feed a single device-wide data path, which feeds the one external interface. The column path is therefore replicated per group rather than per bank or per device.Bank group 04 banksColumn path 0group-localDevice data pathall groups shareInterfaceone, device-wideBank group 14 banksColumn path 1group-localsharesshares12
Figure 1 — the column data path is replicated per group, not per bank and not per device.

Three sharing scopes, three different behaviours, and this figure is the whole chapter: the banks inside one group share a column path; the groups share a device data path; and everything shares one interface. Each level of sharing is a level at which two accesses can collide, and they collide progressively harder as you move right.

2. Generation Qualification, Stated Plainly

Bank groups are not a universal DRAM feature, and treating them as one is a common error.

GenerationBank groups?Organisation
SDR, DDR1, DDR2, DDR3NoBanks only; the column path is device-scoped
DDR4 x4 / x8Yes4 groups × 4 banks = 16 banks
DDR4 x16Yes2 groups × 4 banks = 8 banks
DDR5 x4 / x8Yes8 groups × 4 banks = 32 banks
DDR5 x16Yes4 groups × 4 banks = 16 banks

Three observations worth drawing out.

Before DDR4, the column path was shared device-wide. So all banks contended for it equally, and there was no classification to make: two accesses either hit the same bank or they did not. Bank groups did not add contention — they subdivided contention that already existed, which is a much more accurate way to describe the change than "DDR4 added a constraint."

The banks-per-group count is 4 in every row. What changes across generations and widths is the number of groups, which is the number of column-path copies. That is consistent with §1's framing: the group count is how many copies were affordable.

And the x16 device has fewer groups in both generations, exactly as Chapter 5.1 §4 observed for banks. A wider device moves more bits per column access, so it needs fewer independent column paths to keep its share of the interface busy.

Other DRAM families adopt the concept on their own schedule, and their organisations are covered by Module 24 for LPDDR and Module 26 for HBM. Do not assume a bank-group structure from one family applies to another.

3. The Classification, and One Dangerous Ambiguity

Because the contention depends on the relationship between two accesses rather than on either one alone, the useful output is a classification of a request against its predecessor:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   SAME BANK                  -- same group AND same bank
   SAME GROUP, DIFFERENT BANK -- shares the column path
   DIFFERENT GROUP            -- independent column paths

Exhaustive and mutually exclusive, which §6 asserts.

4. Where This Classification Goes

The classification is not an end in itself. It is the input to things later modules build:

Modules 13 and 14 attach minimum separations to each class. The parameter names and values are theirs; the classes are this chapter's. Module 17 builds a scheduler that reorders requests to produce favourable classifications. Module 18 chooses an address mapping that makes consecutive addresses land in different groups. Chapter 5.7 composes it with bank state and rank ownership into one structural front end.

That is why §5's RTL contains no timing. A classifier that emitted cycle counts would bake one generation's values into a structural function, and the structure outlives the values — DDR4 and DDR5 classify identically and have different parameters. Getting the layering right here is what lets every later module reuse this without modification.

5. RTL — Classifying a Request Against Its Predecessor

Engineering problem

Given a request's bank group and bank, and the group and bank of the previously issued request, classify the relationship into exactly one of three cases — and handle the case where there is no predecessor, which occurs after reset and after any idle period.

Then count each class, because Chapter 4.5 §10 established that a controller which cannot measure its own access-pattern structure cannot be tuned in reasonable time.

Classification

SYNTHESIZABLE RTL. A comparator, a small register holding the predecessor, and saturating counters. Every memory controller contains this in some form.

It contains no timing, deliberately. No cycle counts, no minimum separations, no gap parameters. Chapter 4.5's bank_group_spacer had those and was explicitly labelled as using abstract stand-ins; this block is the layer below, producing the structural fact that a timing layer then acts on. Mixing them would bake one generation's values into a function that is generation-independent.

And it models no physical structure. No column path, no sense amplifiers, no data. It says which scope two accesses contend at; it does not model the contending.

Interface

req_valid with req_bg and req_ba presents a request. issue says the request is actually being issued, so it becomes the new predecessor. Outputs are the three one-hot class flags plus a req_class code, class_valid, and three saturating counters.

State

The previous issued group and bank, plus a prev_valid bit — and the three counters. prev_valid is the part that is easy to omit and important, because a classification against a predecessor that does not exist is meaningless rather than merely wrong.

Combinational logic

Two equality comparisons, decoded three ways.

Sequential logic

The predecessor register updates only on issue, not on req_valid — a request that is presented and not issued must not become the predecessor, because nothing contended with anything.

Simulation

vlog bank_group_classifier.sv tb_bank_group_classifier.sv then vsim -c tb_bank_group_classifier -do "run -all"; VCS vcs -sverilog bank_group_classifier.sv tb_bank_group_classifier.sv && ./simv; Xcelium xrun -sv bank_group_classifier.sv tb_bank_group_classifier.sv.

Expected output: the §7 stream produces one unclassified first request, then a same-bank, a same-group, and a sequence of different-group classifications, with the three counters summing to the number of classified requests.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// BANK GROUP CLASSIFIER.  Classification: SYNTHESIZABLE RTL.
//
// Classifies a request against the PREVIOUSLY ISSUED request into exactly
// one of three structural relationships:
//
//   SAME_BANK        -- same group and same bank
//   SAME_GROUP       -- same group, different bank: shares the column path
//   DIFF_GROUP       -- different group: independent column paths
//
// CONTAINS NO TIMING, DELIBERATELY. No cycle counts, no minimum
// separations. Chapter 4.5's bank_group_spacer had abstract gap parameters
// and sat one layer ABOVE this: it consumed a structural fact and applied a
// timing policy. The classification is generation-independent -- DDR4 and
// DDR5 classify identically and have different timing values -- so baking
// values in here would make a reusable function generation-specific.
//
// Modules 13/14 own the timing parameters. Module 17 owns the scheduler
// that reorders requests to produce favourable classifications. Module 18
// owns the address mapping that decides which group an address lands in.
//
// MODELS NO PHYSICAL STRUCTURE: no column path, no sense amplifiers, no
// data. It names the SCOPE two accesses contend at.
// ─────────────────────────────────────────────────────────────────────────
module bank_group_classifier #(
  parameter int BANK_GROUPS     = 4,
  parameter int BANKS_PER_GROUP = 4,
  parameter int ACC_W           = 16,
  parameter int BG_W = (BANK_GROUPS     <= 1) ? 1 : $clog2(BANK_GROUPS),
  parameter int BA_W = (BANKS_PER_GROUP <= 1) ? 1 : $clog2(BANKS_PER_GROUP)
) (
  input  logic              clk,
  input  logic              rst_n,

  input  logic              req_valid,
  input  logic [BG_W-1:0]   req_bg,
  input  logic [BA_W-1:0]   req_ba,
  // The request is actually being issued, so it becomes the predecessor.
  // Separate from req_valid because a request that is merely PRESENTED and
  // then rejected contended with nothing and must not displace the real
  // predecessor.
  input  logic              issue,

  output logic              class_valid,
  // 0 SAME_BANK, 1 SAME_GROUP, 2 DIFF_GROUP.
  output logic [1:0]        req_class,
  output logic              same_bank,
  output logic              same_group_diff_bank,
  output logic              different_group,

  output logic              index_invalid,

  // Access-pattern telemetry. Saturating: a wrapped counter reports a
  // flattering pattern, and an instrument that errs in the direction its
  // reader hopes for is worse than no instrument.
  output logic [ACC_W-1:0]  cnt_same_bank,
  output logic [ACC_W-1:0]  cnt_same_group,
  output logic [ACC_W-1:0]  cnt_diff_group
);

  localparam logic [1:0] CLS_SAME_BANK  = 2'd0;
  localparam logic [1:0] CLS_SAME_GROUP = 2'd1;
  localparam logic [1:0] CLS_DIFF_GROUP = 2'd2;

  // ── COMPILE-TIME legality.
  if (BANK_GROUPS < 1) begin : g_bg_min
    initial $fatal(1, "bank_group_classifier: BANK_GROUPS must be >= 1");
  end
  if (BANKS_PER_GROUP < 1) begin : g_ba_min
    initial $fatal(1, "bank_group_classifier: BANKS_PER_GROUP must be >= 1");
  end

  // ── Index range checks, using Chapter 5.1's pattern. NOT a cast of the
  //    count to the field's own width, which truncates for powers of two.
  logic bg_bad, ba_bad;
  if (BANK_GROUPS >= (1 << BG_W)) begin : g_bg_full
    assign bg_bad = 1'b0;
  end else begin : g_bg_check
    assign bg_bad = ({1'b0, req_bg} >= (BG_W+1)'(BANK_GROUPS));
  end
  if (BANKS_PER_GROUP >= (1 << BA_W)) begin : g_ba_full
    assign ba_bad = 1'b0;
  end else begin : g_ba_check
    assign ba_bad = ({1'b0, req_ba} >= (BA_W+1)'(BANKS_PER_GROUP));
  end
  assign index_invalid = req_valid && (bg_bad || ba_bad);

  // ── Predecessor state.
  logic [BG_W-1:0] prev_bg;
  logic [BA_W-1:0] prev_ba;
  // Without this, the first request after reset would classify against a
  // zeroed register and report SAME_BANK against a predecessor that never
  // existed -- a wrong answer that looks entirely plausible in a trace.
  logic            prev_valid;

  logic bg_eq, ba_eq, can_classify;
  assign bg_eq        = (req_bg == prev_bg);
  assign ba_eq        = (req_ba == prev_ba);
  assign can_classify = req_valid && prev_valid && !index_invalid;

  assign same_bank            = can_classify &&  bg_eq &&  ba_eq;
  assign same_group_diff_bank = can_classify &&  bg_eq && !ba_eq;
  assign different_group      = can_classify && !bg_eq;

  assign class_valid = can_classify;
  assign req_class   = same_bank            ? CLS_SAME_BANK
                     : same_group_diff_bank ? CLS_SAME_GROUP
                                            : CLS_DIFF_GROUP;

  // ── Saturating counter increments, one bit wide so the carry is the
  //    overflow flag.
  logic [ACC_W:0] sb_sum, sg_sum, dg_sum;
  always_comb begin
    sb_sum = {1'b0, cnt_same_bank}  + (ACC_W+1)'(1);
    sg_sum = {1'b0, cnt_same_group} + (ACC_W+1)'(1);
    dg_sum = {1'b0, cnt_diff_group} + (ACC_W+1)'(1);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      prev_bg        <= '0;
      prev_ba        <= '0;
      prev_valid     <= 1'b0;
      cnt_same_bank  <= '0;
      cnt_same_group <= '0;
      cnt_diff_group <= '0;
    end else begin
      // Count the classification of a request that is actually ISSUED.
      // Counting merely-presented requests would measure the scheduler's
      // input rather than the device's access pattern, which are different
      // things and the second is the one that matters.
      if (issue && class_valid) begin
        unique case (req_class)
          CLS_SAME_BANK:  cnt_same_bank  <= sb_sum[ACC_W] ? {ACC_W{1'b1}} : sb_sum[ACC_W-1:0];
          CLS_SAME_GROUP: cnt_same_group <= sg_sum[ACC_W] ? {ACC_W{1'b1}} : sg_sum[ACC_W-1:0];
          default:        cnt_diff_group <= dg_sum[ACC_W] ? {ACC_W{1'b1}} : dg_sum[ACC_W-1:0];
        endcase
      end

      // Update the predecessor only on a genuine issue.
      if (issue && req_valid && !index_invalid) begin
        prev_bg    <= req_bg;
        prev_ba    <= req_ba;
        prev_valid <= 1'b1;
      end
    end
  end

endmodule

Cycle trace

BANK_GROUPS = 4, BANKS_PER_GROUP = 4, issue held high:

Cyclereq_bgreq_baclass_validClassWhy
0000no predecessor
1001SAME_BANKsame group, same bank
2011SAME_GROUPsame group, different bank
3111DIFF_GROUPgroup changed
4111SAME_BANKpredecessor was (1,1)

Cycle 4 is the one to study. The request (1,1) is classified SAME_BANK, and the identical request at cycle 3 was classified DIFF_GROUP. The classification is a property of the pair, not of the request — which is why §7's waveform matters and why a per-request trace that omits the predecessor cannot reproduce this.

Waveform expectation

§7. Watch that identical consecutive req_bg/req_ba values produce different classes depending on what preceded them.

Synthesis implication

Two equality comparators of BG_W and BA_W bits, a few gates of decode, BG_W + BA_W + 1 flip-flops for the predecessor, and three saturating counters. Tiny — and the classification is purely combinational from registered state, so it is available early in a cycle, which matters because a scheduler wants it before deciding.

Corner cases

BANK_GROUPS == 1 makes different_group permanently false and every classification SAME_BANK or SAME_GROUP — which is pre-DDR4 behaviour, and reproducing it at that parameter is a useful self-check that the block is genuinely parameterised. BANKS_PER_GROUP == 1 makes same_group_diff_bank impossible. Both give a 1-bit field through the guards, and BANK_GROUPS == 1 then has one legal encoding of two, so its range check materialises — 5.1 §5's degenerate case yet again. A request presented with issue low classifies but does not become the predecessor and is not counted. An invalid index suppresses classification entirely rather than classifying against a garbage comparison.

Verification

What DV must prove: exhaustiveness and mutual exclusivity — exactly one class flag whenever class_valid, never two, never none; class_valid low until the first issue after reset; the predecessor updating on issue and not on a non-issued req_valid; counters matching an independent tally; saturation rather than wrap; and the degenerate parameter cases behaving as described.

Debugging

If the first request after reset classifies as SAME_BANK, prev_valid is missing or is being set at reset — the classic omission, and it produces a completely plausible-looking trace. If classifications look right but the counts are wrong, check that counting is gated on issue rather than on req_valid; counting presented requests measures the scheduler's input, not the device's access pattern. If a rejected request corrupts subsequent classifications, the predecessor register is updating on req_valid. If different_group never fires at BANK_GROUPS > 1, check that req_bg is actually varying — an address mapping that leaves the group field constant produces exactly this, and it is a system bug that looks like a block bug.

Limitations

No timing, as the header states at length. No notion of how long ago the predecessor was issued — a request far removed in time from its predecessor contends with nothing in reality, and this block would still classify it. That is a genuine simplification: the real constraint is "within a minimum separation of", and adding the time dimension is exactly what Modules 13 and 14 do. No modelling of more than one outstanding predecessor, where a real device may have several accesses in flight and contention is not purely pairwise. No row state — 5.2 owns that, and §3's callout explains why keeping them separate matters. And no reordering: this block classifies the request it is given, where a scheduler's value is largely in choosing which request to give it.

6. Four Assertions Worth Writing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY, inside bank_group_classifier.

// P1 -- MUTUAL EXCLUSIVITY. The three classes describe one relationship, so
// at most one can hold. A classification that can report two things at once
// misdirects every consumer downstream -- and the consumers here are the
// timing layer and the scheduler.
property p_classes_exclusive;
  @(posedge clk) disable iff (!rst_n)
    $onehot0({same_bank, same_group_diff_bank, different_group});
endproperty
assert property (p_classes_exclusive);

// P2 -- EXHAUSTIVENESS, the necessary companion. P1 alone is satisfied by a
// block that classifies nothing, which is the vacuity trap this curriculum
// keeps meeting. Whenever class_valid holds, EXACTLY one class must.
property p_classes_exhaustive;
  @(posedge clk) disable iff (!rst_n)
    class_valid |-> $onehot({same_bank, same_group_diff_bank, different_group});
endproperty
assert property (p_classes_exhaustive);

// P3 -- the structural implication between classes. SAME_BANK necessarily
// implies the same group, because a bank lives in exactly one group. This
// looks redundant against the definitions and is not: it is a statement
// about the HIERARCHY rather than about the comparison logic, and it fails
// if someone ever "optimises" same_bank to skip the group comparison --
// which would then match bank N of EVERY group.
property p_same_bank_implies_same_group;
  @(posedge clk) disable iff (!rst_n)
    same_bank |-> bg_eq;
endproperty
assert property (p_same_bank_implies_same_group);

property p_diff_group_excludes_same_bank;
  @(posedge clk) disable iff (!rst_n)
    different_group |-> !same_bank;
endproperty
assert property (p_diff_group_excludes_same_bank);

// P4 -- the predecessor is the last ISSUED request, not the last presented
// one. Protects the block's central contract: contention is with something
// that actually happened.
property p_prev_tracks_issue_only;
  @(posedge clk) disable iff (!rst_n)
    (req_valid && !issue)
      |=> ((prev_bg == $past(prev_bg)) && (prev_ba == $past(prev_ba)));
endproperty
assert property (p_prev_tracks_issue_only);

// P5 -- no classification before a predecessor exists.
property p_no_class_without_prev;
  @(posedge clk) disable iff (!rst_n)
    !prev_valid |-> !class_valid;
endproperty
assert property (p_no_class_without_prev);

P3 is the one that looks redundant and is not, and it is worth understanding why. Read against the code, same_bank already includes bg_eq, so P3 seems to restate a conjunct. But P3 is a claim about the hierarchy, not about the code: a bank belongs to exactly one group, so identifying a bank requires both fields. If someone later "optimises" the comparison to test only req_ba == prev_ba — reasoning that bank numbers are unique — the classifier would match bank N of every group, and P3 is the only property here that fails.

The general lesson: assert the structural invariants, not only the logic's outputs. A property that restates the implementation catches nothing when the implementation changes; a property that states a fact about the domain catches exactly the changes that violate the domain.

P2 is the exhaustiveness companion to P1 — the same vacuity guard as 5.1's P3 and 5.2's P5. Three chapters, three instances: restrictive properties always need a companion that requires something.

What none of them prove. Nothing about timing — whether the classified contention actually costs anything is Modules 13 and 14' question and this block has no notion of it. Nothing about whether the group field itself is correct, which is address mapping and Module 18's. And nothing about row state: a SAME_BANK classification says nothing about whether the access is a row hit, and §3's callout exists because that confusion is easy and expensive.

7. Classification Depends on History

bank_group_classifier — a request stream and its structural classification

10 cycles
Ten cycles of requests with their bank group and bank. The first request cannot be classified because no predecessor exists. A repeat of the same bank classifies as same bank. A different bank in the same group classifies as same group. Moving to another group classifies as different group. A repeat of that request then classifies as same bank, showing that the identical request receives different classifications depending on its predecessor. The different-group counter accumulates to three.unclassifiableunclassi…inside group 0inside group 0moving between groupsmoving between groupsno predecessor yetno predecessor yetsame request, new classsame request, new class4 of 9 crossed groups4 of 9 crossed groupsclkreq_bg0001122230req_ba0011101130class_validsame_banksame_group_diff_bankdifferent_groupcnt_diff_group0000112223t0t1t2t3t4t5t6t7t8t9
Figure 2 — the same request classifies differently depending on what preceded it.

Cycle 0 has no classification at all, and that is correct rather than a gap. There is nothing to contend with. A block missing prev_valid would report SAME_BANK here — against a zeroed register describing a request that never happened — and the trace would look entirely reasonable.

Cycles 3 and 4 are the chapter's point in two cycles. Both requests are (1,1). Cycle 3 classifies DIFF_GROUP; cycle 4 classifies SAME_BANK. Nothing about the request changed. The classification is a property of the pair, which is why a trace recording only per-request fields cannot diagnose group contention, and why 5.1 §9's debugging lesson — capture the predecessor alongside each request — was worth stating early.

The counter is the practical output. Four of the nine classified requests crossed a group boundary. A controller that can report that ratio can be tuned; one that cannot, cannot — and the ratio is a property of the address mapping and the workload together, not of the device.

8. The Bank Group, Answered Systematically

QuestionAnswer for a bank group
What does it contain?A set of banks — 4 per group in every verified DDR4 and DDR5 organisation
What resource does it share?The internal column data path, plus everything device-wide below it
What can operate in parallel with it?Column activity in another group; row activity anywhere
How is it selected?A bank-group field in the request, alongside the bank field
What must the controller track?The previously issued group and bank — history, not state
What opportunity does it create?Interleaving across groups sustains a higher column-access rate
What conflict does it create?Consecutive accesses in one group contend for its column path
What must DV verify?Exhaustive, mutually exclusive classification, and that "previous" means issued

The "what must the controller track" row is the one that differs from every other level. Banks, ranks and channels all require the controller to track state — what is open, who owns what, what is queued. A bank group requires it to track history: which group the last issued request went to. That is a genuinely different kind of obligation, and it is why the classification is pairwise rather than absolute.

9. Common Misconceptions

"A bank group is just a naming convention for a set of banks." Wrong mental model: grouping is organisational labelling over a larger bank count. Engineering action: treating all bank pairs as equally independent; ignoring the group field when mapping addresses; modelling concurrency purely by bank count. Observable failure / bad conclusion: a performance model that over-predicts, and — the practical version — an address mapping that concentrates consecutive accesses into one group, which costs a large fraction of the device's achievable rate with nothing malfunctioning. Chapter 4.5 §10 identified this as the most common DDR4-specific defect. Correct model: a bank group is the scope at which the internal column data path is shared. Banks in one group contend for it; banks in different groups use independent copies. The level exists because that path was too expensive to replicate per bank and too limiting to leave device-wide. Prevention: ask what is shared at the level. If nothing is, the level would not exist — hierarchy levels in DRAM are not free.

"Bank groups added a constraint that DDR3 did not have." Wrong mental model: DDR4 made memory harder by introducing group timing. Engineering action: treating bank groups as a regression to work around; assuming DDR3's uniform bank timing was structurally simpler and therefore better. Observable failure / bad conclusion: misunderstanding the entire trade, and an inability to explain why DDR4's rate improved at all if the constraint got worse. Correct model: before DDR4 the column path was shared device-wide, so every pair of banks contended. Bank groups subdivided contention that already existed, by replicating the path some number of times. The cross-group case is new and better; the same-group case is what used to apply everywhere. Prevention: ask what the pre-DDR4 device did. If the resource was shared device-wide, adding groups strictly increased the available parallelism.

"'Same bank' means a row conflict." Wrong mental model: the classification and the row-state question are the same thing. Engineering action: attributing group-contention cost to row conflicts or the reverse; applying an address-mapping fix aimed at the wrong mechanism. Observable failure / bad conclusion: a remapping that reduces row conflicts and leaves group contention untouched, or vice versa — and since both are address-mapping decisions that can pull in opposite directions, optimising for one can worsen the other. Correct model: they are orthogonal. "Same bank" as a classification means same bank as the previously issued request and depends on history. A row conflict means same bank, different row than the one currently open and depends on state. All four combinations occur. Prevention: never say "same bank" without saying same bank as what — as the predecessor, or as the open row's bank.

"All DRAM has bank groups." Wrong mental model: bank groups are a general DRAM feature. Engineering action: assuming a group field exists in every device; carrying a DDR4 or DDR5 structure into reasoning about a different generation or family; writing a controller model that requires a group field. Observable failure / bad conclusion: a model that cannot represent DDR3 or earlier at all, and incorrect assumptions carried into LPDDR or HBM, whose organisations are Modules 24 and 26' subject and are not interchangeable with DDR's. Correct model: bank groups appear in DDR4 and DDR5. SDR, DDR1, DDR2 and DDR3 have banks only, with a device-scoped column path. Other families adopt the concept on their own schedule. Prevention: set BANK_GROUPS = 1 in §5's block and confirm it degenerates correctly. A model that cannot represent the one-group case is over-fitted to one generation.

10. Debugging — Requests Serialise Despite Many Banks Being Available

Symptom. A workload spreads well across banks — the per-bank open-row table shows many banks in use and row hits are common — and throughput is still far below what bank count suggests. No errors.

Good bank spread with poor throughput points below the bank level, which is a useful narrowing: whatever is limiting is shared by banks that are individually behaving well.

Mechanism 1 — the spread is across banks but within one group. Inspect: the same-group versus different-group counts from §5's telemetry. Expected evidence: a high cnt_same_group and near-zero cnt_diff_group despite many distinct banks being touched. Discriminator: the classification ratio, not the bank distribution. This is first because it is the most common cause and because bank spread and group spread look identical in a bank histogram — touching 4 banks in one group and 4 banks across 4 groups produce the same "4 banks in use" figure and completely different throughput. A bank histogram cannot diagnose this; only the pairwise classification can.

Mechanism 2 — the address mapping leaves the group field constant. Inspect: which address bits carry the group field, against the workload's stride. Expected evidence: req_bg essentially never changing across a long request stream. Discriminator: is the group field varying at all? Distinguished from mechanism 1 by cause rather than symptom: mechanism 1's group field varies but the scheduler is not exploiting it, while here the field is constant before the scheduler ever sees it. Only the second is fixed in Module 18's mapping.

Mechanism 3 — the classification is measured against presented rather than issued requests. Inspect: whether the predecessor register updates on issue or on req_valid. Expected evidence: telemetry showing a healthy cross-group ratio that does not match measured throughput. Discriminator: does the instrument disagree with the outcome? This is the case where the measurement is the bug — §5's debugging note — and it is worth checking early precisely because it invalidates everything else you would conclude from the counters.

Mechanism 4 — the limit is below the group: the device data path or the interface. Inspect: interface utilisation, and whether it is saturated. Expected evidence: a healthy cross-group ratio and an interface that is busy essentially all the time. Discriminator: is the interface busy or idle? Chapter 5.2 §3 established that everything shares one interface; if it is saturated, no amount of group interleaving helps and the ceiling has simply been reached. This is the mechanism where the answer is "nothing is wrong".

Mechanism 5 — not structural: read/write turnaround. Inspect: the direction mix and whether the stream alternates frequently. Expected evidence: throughput strongly dependent on the read/write ratio, with mixed streams far worse than either pure one. Discriminator: split the workload into pure-read and pure-write runs. If each is fast and the mix is slow, the cost is direction changes on the shared bidirectional data path — Chapter 4.2 §3's turnaround — and the fix is batching by direction, which no amount of group or bank spreading achieves.

Discrimination, cheapest first. Read the classification counters — one observation, and their ratio separates mechanisms 1 and 2 from everything below. Then confirm the counters are gated on issue. Then read interface utilisation, which splits "not exploiting the structure" from "at the ceiling". Then split by direction.

The reasoning lesson. A per-request histogram and a pairwise classification measure different things, and only one of them can see group contention. Touching many distinct banks looks healthy in every per-request metric and can still be pathological, because the cost depends on consecutive pairs. When a resource is shared at a scope, the diagnostic must be expressed at that scope — and for bank groups the scope is a pair of consecutive requests, which no amount of aggregating individual requests will reveal. This is the same lesson 5.1 §9 drew about sequence-dependent costs, now with a concrete instrument attached.

11. Interview Reasoning

"If banks are already independent, why do bank groups matter?" Because banks are independent in row state and not in everything. Serving a column access needs internal column-selection and data-path circuitry, and that machinery is not replicated per bank — it is too large to duplicate for a resource that is busy only briefly per access. So it is shared by a set of banks, and that set is a bank group. Two accesses in the same group contend for it and must be separated further in time; two accesses in different groups use independent copies. A bank group is therefore the scope at which the column path is shared, not merely a container for banks.

"Did DDR4 make memory harder by adding bank-group timing?" No — it subdivided contention that already existed. Before DDR4 the column path was shared device-wide, so every pair of banks contended for it equally. Bank groups replicated that path some number of times, so the cross-group case is new and strictly better, and the same-group case is what previously applied everywhere. Reading it as an added constraint makes DDR4's rate improvement inexplicable: the whole point was to get rate from overlap once prefetch depth could not grow.

"What is the difference between 'same bank' as a classification and a row conflict?" They are orthogonal and both involve the phrase "same bank". The classification means the current request targets the same bank as the previously issued request, so it depends on history and is about contending for shared resources. A row conflict means the access targets a bank whose currently open row is not the one wanted, so it depends on state and forces a close and an open. All four combinations occur — you can have a same-bank classification on a row hit, which is the fastest case, or a different-bank access that is still a row conflict. They matter separately because both are addressed through address mapping and the two fixes can pull in opposite directions.

"Why should a bank-group classifier contain no timing?" Because the classification is generation-independent and the timing is not. DDR4 and DDR5 classify a pair of accesses identically and attach different minimum separations to the classes, and the separations also vary by speed grade. Putting cycle counts in the classifier would make a reusable structural function generation-specific, and it would conflate two layers: the structural fact that two accesses share a column path, and the policy about how far apart they must therefore be. Keeping them separate is what lets the scheduler, the timing layer and the address-mapping layer all consume the same classification.

"A workload touches many banks and still serialises. What do you measure?" The pairwise classification, not the bank histogram — because those two can disagree completely. Touching four banks within one bank group and four banks across four groups both show "four banks in use", and they perform very differently, since the cost depends on consecutive pairs rather than on the set of banks touched. So read the same-group versus different-group counts. If the ratio is poor, ask whether the group field is varying at all, which separates a scheduler that is not exploiting the structure from an address mapping that destroyed it upstream. If the ratio is good and throughput is still low, check interface utilisation — if the interface is saturated, the ceiling has been reached and nothing about groups will help.

12. Engineering Check

A DDR4 x8 device: 4 bank groups × 4 banks = 16 banks, as verified in §2. Classify each consecutive pair in the streams below. Bank identifiers are written as (group, bank).

1. Stream A: (0,0) → (0,1) → (0,2) → (0,3). Classify each pair. Three pairs, all SAME_GROUP, different bank. Four distinct banks touched and every consecutive pair contends for group 0's column path. A bank histogram would report perfect spread across four banks.

2. Stream B: (0,0) → (1,0) → (2,0) → (3,0). Classify each pair. Three pairs, all DIFFERENT_GROUP. Also four distinct banks — and every consecutive pair uses an independent column path.

3. Streams A and B touch the same number of banks. Why do they perform differently? Because the cost depends on consecutive pairs, not on the set of banks touched. A and B have identical bank-count statistics and opposite classification statistics. This is why §10's first mechanism insists on the pairwise classification: no aggregate over individual requests can distinguish these two streams.

4. Stream C: (0,0) → (1,0) → (0,0) → (1,0). Classify, and note anything about row state. Three pairs, all DIFFERENT_GROUP — excellent classification. But note what the classification does not tell you: whether each access is a row hit. If (0,0) is accessed with two different rows alternately, every access to it is also a row conflict, and the stream is simultaneously best-case for group contention and worst-case for row state. §3's orthogonality, concretely.

5. Which address bits should carry the group field to turn stream A into stream B? Bits that change most rapidly across the workload's access sequence — so that consecutive addresses land in different groups. In stream A the group field is constant while the bank field varies, meaning the mapping put the group field on slower-changing bits than the bank field. Swapping their positions inverts the behaviour entirely with no hardware change. The actual choice is Module 18's, and the right answer depends on the workload's stride, which is why there is no universal mapping.

6. Set BANK_GROUPS = 1 in §5's classifier. What do streams A and B classify as, and which generation is that? With one group, different_group is impossible — every pair is SAME_BANK or SAME_GROUP. Stream B, which was all-different-group, becomes all-same-group. That is DDR3 and earlier, where the column path was device-wide and all banks contended equally. It also shows exactly what DDR4 bought: the same stream B, on the same access pattern, moved from the contended class to the independent class purely because the path was replicated four times.

13. Summary

A bank group is the scope at which the internal column data path is shared — not a container for banks. Serving a column access needs column-selection and data-path circuitry that is too large to replicate per bank and too limiting to leave device-wide, so it is replicated some number of times. The group is the unit of that partial replication, and the group count is how many copies were affordable.

Two accesses in the same group contend for that path; two in different groups use independent copies. That is the whole behaviour, and every timing parameter, scheduling strategy and mapping recommendation follows from it.

Bank groups are DDR4 and DDR5 only. Verified organisations: DDR4 x4/x8 has 4 groups × 4 banks = 16; x16 has 2 × 4 = 8; DDR5 x4/x8 has 8 × 4 = 32; x16 has 4 × 4 = 16. Banks per group is 4 in every case, so what varies is the number of column-path copies. Before DDR4 the path was device-wide — so bank groups subdivided contention that already existed rather than adding a constraint.

The useful output is a three-way classification against the previously issued request: SAME_BANK, SAME_GROUP different bank, DIFFERENT_GROUP — exhaustive and mutually exclusive. It is a property of the pair, not of the request: the identical request classifies differently depending on what preceded it.

"Same bank" means two different things and they are orthogonal. As a classification it means same bank as the predecessor and depends on history. As a row conflict it means same bank, different open row and depends on state. All four combinations occur, both are addressed through address mapping, and the two fixes can pull in opposite directions.

The classification carries no timing, deliberately. It is generation-independent; the minimum separations attached to each class are not. Keeping the layers separate is what lets Modules 13, 14, 17 and 18 all consume the same structural fact.

And a bank group is the one level that requires the controller to track history rather than state — which is why a per-request histogram cannot see group contention at all, and why the diagnostic must be expressed over consecutive pairs.

14. What Comes Next

Everything so far has been inside one device. Chapter 5.4 leaves it.

A memory controller needs a data path wider than any single DRAM device provides, so several devices operate together, each contributing part of the width. That set is a rank — and the interesting part is not the arithmetic of adding widths but the consequence: multiple ranks share one physical data bus, so only one of them may drive it at a time.

That turns the rank into a question of resource ownership rather than of selection, and it is the same reasoning pattern Chapter 3.5 used for sense amplifiers, now at the scale of a board.

Return to Banks for the row state this chapter's classification is orthogonal to, DDR4 for why bank groups were introduced, or The DDR Device Structure for the sharing-scope framing. Module 16 is the full treatment. The full path is on the DDR tutorials index.

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.