A fix is a rule restored, not a seed that now passes. Each defect in Find the Races broke one rule the course states; the fix puts that rule back where the defect removed it.
Your task | Detail |
|---|---|
Build | The three fixes, a directed script for each arrival order of each race, and a message and memory-write counter for MESI and MOESI |
Interface | run(seed, script=..., log=[]) for the traces, resp_size=1 or None for the response slots, run(0, script=trace, drain=True) for MESI traffic, count(proto, trace) for the comparison |
Done when | Each script passes every seed with the defects off and fails with its defect on (where its order exposes the bug), both response sizes pass, and every traffic number is asserted |
Try the fixes and the counter yourself first; the worked solution follows.
Defect | Rule restored | Where in the model | Hardware structure |
|---|---|---|---|
a. lost-inv | H invalidates every recorded sharer except the requester and grants M only when every Inv-Ack is in. A sharer bit is cleared only by its Inv-Ack or its Put. | enter() no longer touches the sharer set; GetM counts Invs, each Inv-Ack removes its sender, and done() grants at acks 0 | Sharer vector plus an ack counter in the pending entry |
b. late-put | A Put from a cache H does not record as a holder gets Put-Ack with no directory or memory change. The cache keeps its WB entry until Put-Ack. | The Put handler requires own == sender; Put-Ack is the only event that frees MI^A, EI^A, SI^A, II^A | Holder check in H's Put path; WB entry freed on Put-Ack |
c. shared-queue | Responses must always sink: they have their own class and reserved slots, so consuming a response never waits for a request. | room(): a response enters while fewer than resp_size responses are in H's buffer; requests count only against the request slots | A response virtual channel with its own reserved slots at H (n - 1 here) |
Fix (c) sizes the reserved space from the protocol, not by guess. One reserved slot suffices for progress, because H consumes responses unconditionally: a response is never held behind the pending entry or a request, so a waiting response always gets the slot back. n - 1 slots, the most responses one transaction can be owed (Inv-Acks from the other caches, or one Data), means no response ever waits. The block below asserts both, and that with no response slot the progress check fires. With more lines and pending entries, reserve per pending entry when it is allocated, as Message Classes and Virtual Networks explains.
Fixed trace (a) is the race of Simultaneous Upgrades with C0 winning; it is not redrawn. Script A2 is the same race with C1 winning, and passes too. Fixed trace (b) keeps the roles and step numbers of the defective trace up to step 4 and the assumed delivery; from step 5 the correct H behaves as follows.
Step | C0 state / value | C1 state / value | H directory | Memory | Newest data |
|---|---|---|---|---|---|
(assumed) grant delivered | II^A / - | M / 2 | exclusive(C1) | 0 | C1: 2 |
5. late PutM at H | II^A / - | M / 2 | exclusive(C1) | 0 | C1: 2 |
6. Put-Ack at C0 | I / - | M / 2 | exclusive(C1) | 0 | C1: 2 |
L. C0 load | IS^D / - | M / 2 | exclusive(C1) | 0 | C1: 2 |
7. GetS at H | IS^D / - | M / 2 | exclusive(C1), pend GetS C0 | 0 | C1: 2 |
8. Fwd-GetS at C1 | IS^D / - | S / 2 | exclusive(C1), pend GetS C0 | 0 | C1: 2, Data(2) in flight to H |
9. Data at H | IS^D / - | S / 2 | exclusive(C1), pend GetS C0 | 0 | C1: 2, H buffer: 2 |
H writes memory | IS^D / - | S / 2 | S{C1}, pend GetS C0 | 2 | C1 and memory: 2, Data(2) in flight to C0 |
10. Data at C0 | S / 2 | S / 2 | S{C1}, pend GetS C0 | 2 | C0, C1 and memory: 2 |
(assumed) grant delivered | S / 2 | S / 2 | S{C0,C1} | 2 | C0, C1 and memory: 2 |
Script B2 is the other arrival order, Part 4's order A: the PutM reaches H before the GetM, H writes memory = 1 and records U, and C1's GetM is served from memory. Fixed trace (c) keeps steps 1 to 4. The Inv-Ack now enters a reserved response slot although both request slots are full.
Step | C0 state / value | C1 state / value | C2 state / value | H directory | Memory | Newest data |
|---|---|---|---|---|---|---|
4. Inv at C1 | SM^D / 0 | IM^D / - | IS^D / - | S{C0,C1}, pend GetM C0, acks 0/1, queued GetM C1, GetS C2 | 0 | C0 and memory: 0 |
5. Inv-Ack at H (response slot) | SM^D / 0 | IM^D / - | IS^D / - | S{C0}, pend GetM C0, acks 1/1, queued GetM C1, GetS C2 | 0 | C0 and memory: 0, Data (permission only) in flight to C0 |
6. Data at C0, store | M / 1 | IM^D / - | IS^D / - | S{C0}, pend GetM C0, acks 1/1, queued GetM C1, GetS C2 | 0 | C0: 1 |
(assumed) grant delivered | M / 1 | IM^D / - | IS^D / - | exclusive(C0), queued GetM C1, GetS C2 | 0 | C0: 1 |
From here H may serve B or R first, and the block below shows that both orders occur and both complete.
import random
from collections import Counter
REQ = ("GetS", "GetM", "PutS", "PutE", "PutM") # request class, everything else sent to H is a response
STABLE = ("I", "S", "E", "M")
def run(seed, defects=(), steps=4000, n=3, hq_size=2, resp_size=None, script=None, drain=False,
seen=None, log=None, authority=True):
"""Three-core MESI directory model of one line X. Returns (first failure or None, traffic counts)."""
rng, seen = random.Random(seed), Counter() if seen is None else seen
resp_size, log = n - 1 if resp_size is None else resp_size, [] if log is None else log
c = [{"st": "I", "v": None} for _ in range(n)] # I S E M, IS IM SM (the ^D states), SI EI MI II (^A)
H = {"dir": "U", "sh": set(), "own": None, "mem": 0, "busy": None, "tid": 0} # dir U, S or X(own)
to_c, link, hq = [], [], [] # to caches (always sink), waiting to enter H, H's input buffer
gold, nxt, cnt, bad = [0], [0], Counter(), []
def send(kind, src, dst, **kw):
cnt["msgs"] += 1
(link if dst == "H" else to_c).append(dict(kind=kind, src=src, dst=dst, **kw))
def room(m): # may m enter H's input buffer now?
if "shared-queue" in defects: return len(hq) < hq_size # defect c: one shared buffer
if m["kind"] in REQ: return sum(x["kind"] in REQ for x in hq) < hq_size
return sum(x["kind"] not in REQ for x in hq) < resp_size # reserved response slots
def enter(m):
link.remove(m); hq.append(m)
if m["kind"] == "GetM" and H["busy"]: seen["GetM queued behind busy"] += 1
if "lost-inv" in defects and m["kind"] == "GetM": H["sh"].discard(m["src"]) # defect a
def store(i): nxt[0] += 1; c[i]["v"] = gold[0] = nxt[0] # unique values: a stale read is visible
def load(i):
if c[i]["v"] != gold[0]: bad.append(f"value: load at C{i} got {c[i]['v']}, golden {gold[0]}")
def check(): # the golden checker, after EVERY event
w = [i for i in range(n) if c[i]["st"] in ("E", "M")]
r = [i for i in range(n) if c[i]["st"] in ("S", "SM")]
err = bad + [f"value: C{i} holds {c[i]['v']}, golden {gold[0]}" for i in w + r if c[i]["v"] != gold[0]]
if authority: # the authority half: H's record and memory
rec = f"exclusive(C{H['own']})" if H["dir"] == "X" else H["dir"]
err += [f"value: dir {rec} with C{i} in {c[i]['st']}" for i in w if rec != f"exclusive(C{i})"]
if H["dir"] in ("U", "S") and H["mem"] != gold[0]:
err.append(f"value: dir {rec}, memory {H['mem']}, golden {gold[0]}")
if len(w) > 1 or (w and r): err.append(f"SWMR: writers {w}, readers {r}")
if sum(x["kind"] not in REQ for x in hq) > resp_size: err.append("response slots exceeded")
assert not err, "; ".join(err)
def grant(dst, perm, v): send("Data", "H", dst, v=v, perm=perm) # recorded when delivered
def done(t):
if not t.get("sent") and t["acks"] == 0: t["sent"] = True; grant(t["req"], "M", H["mem"])
def home(m): # H handles one message from its input buffer
hq.remove(m); k, s, b = m["kind"], m["src"], H["busy"]
if k == "GetS":
H["busy"] = {"req": s, "op": k}
if H["dir"] == "X": send("Fwd-GetS", "H", H["own"])
else: grant(s, "E" if H["dir"] == "U" else "S", H["mem"])
elif k == "GetM":
assert not (H["dir"] == "X" and H["own"] == s), f"protocol: GetM from holder C{s}"
H["tid"] += 1; t = H["busy"] = {"req": s, "op": k, "id": H["tid"], "acks": 0}
if H["dir"] == "X": send("Fwd-GetM", "H", H["own"])
else:
for o in sorted(H["sh"] - {s}): t["acks"] += 1; send("Inv", "H", o, id=t["id"])
done(t) # grants only when every Inv-Ack is in
elif k == "Inv-Ack":
assert b and b.get("id") == m["id"], f"protocol: unexpected Inv-Ack from C{s}"
H["sh"].discard(s); b["acks"] -= 1; done(b)
elif k == "Data": # newest data from the exclusive holder, after a Fwd
assert b and b.get("op") in ("GetS", "GetM"), f"protocol: unexpected Data from C{s}"
if b["op"] == "GetS": # MESI: a forwarded read also writes memory
H["mem"] = m["v"]; cnt["memwr"] += 1
H.update(dir="S", sh=set() if m["wb"] else {H["own"]}, own=None)
grant(b["req"], "S" if b["op"] == "GetS" else "M", m["v"])
else: # PutS, PutE, PutM
holder = H["dir"] == "X" and H["own"] == s
if holder or ("late-put" in defects and k == "PutM" and H["dir"] == "X"): # defect b
if k == "PutM": H["mem"] = m["v"]; cnt["memwr"] += 1
H.update(dir="U", own=None)
else: # a sharer, or a late Put from a non-holder
if k != "PutS": seen["late " + k] += 1
H["sh"].discard(s)
if H["dir"] == "S" and not H["sh"]: H["dir"] = "U"
send("Put-Ack", "H", s)
def move(i, k, table): # protocol checker: is k legal in this state?
assert c[i]["st"] in table, f"protocol: {k} at C{i} in {c[i]['st']}"
seen[(k, c[i]["st"])] += 1; c[i]["st"] = table[c[i]["st"]]
def cache(m):
to_c.remove(m); i, k, x = m["dst"], m["kind"], c[m["dst"]]
if k == "Data": # course-model assumption: H sees grant delivery,
H["busy"] = None # and records the new holder or sharer then
if m["perm"] == "S": H["dir"] = "S"; H["sh"].add(i)
else: H.update(dir="X", own=i, sh=set())
x["v"] = m["v"]; move(i, k, {"IS": m["perm"], "IM": "M", "SM": "M"})
store(i) if x["st"] == "M" else load(i) # the stalled access completes first
elif k == "Inv":
send("Inv-Ack", i, "H", id=m["id"]); move(i, k, {"S": "I", "SM": "IM", "SI": "II"})
elif k in ("Fwd-GetS", "Fwd-GetM"): # answered from the array or from the WB entry
send("Data", i, "H", v=x["v"], wb=x["st"] in ("MI", "EI"))
move(i, k, {"E": "S", "M": "S", "EI": "SI", "MI": "SI"} if k == "Fwd-GetS"
else {"E": "I", "M": "I", "EI": "II", "MI": "II"})
else: move(i, k, {"MI": "I", "EI": "I", "SI": "I", "II": "I"}) # Put-Ack frees the WB entry
if x["st"] in ("I", "IM", "II"): x["v"] = None
def op(i, what=None):
x = c[i]
if x["st"] not in STABLE + ("SM",): return # one outstanding request per cache
if what is None:
r = rng.random(); what = "load" if r < 0.45 else "store" if r < 0.8 else "evict"
log.append((i, what))
if x["st"] == "SM": # SM^D: loads hit, stores and evictions wait
if what == "load": load(i); seen["load hit in SM^D"] += 1
elif what == "load":
if x["st"] == "I": x["st"] = "IS"; send("GetS", i, "H")
else: load(i)
elif what == "store":
if x["st"] in ("E", "M"): x["st"] = "M"; store(i) # E to M is silent
else: x["st"] = "SM" if x["st"] == "S" else "IM"; send("GetM", i, "H")
elif x["st"] != "I": # notified eviction: the WB entry keeps the value
send({"S": "PutS", "E": "PutE", "M": "PutM"}[x["st"]], i, "H", v=x["v"]); x["st"] += "I"
def moves():
return ([("c", m) for m in to_c] + [("in", m) for m in link if room(m)]
+ [("h", m) for m in hq if m["kind"] not in REQ or not H["busy"]])
def do(mv): # one delivery, logged in script form
m = mv[1]; log.append((m["kind"], m["src"], m["dst"]) + (("wait",) if mv[0] == "in" else ()))
{"c": cache, "in": enter, "h": home}[mv[0]](m)
step = None
try:
for e in script or (): # directed run: core ops and chosen deliveries
if isinstance(e[0], int): op(*e)
else: # (kind, src, dst[, "wait"]): deliver that message
mv = [x for x in moves() if (x[1]["kind"], x[1]["src"], x[1]["dst"]) == e[:3]]
assert mv, f"script: {e} not enabled"
do(mv[0]); check()
if mv[0][0] == "in" and e[3:] != ("wait",): # and let H handle it, if it can
mv = [x for x in moves() if x[0] == "h" and x[1] is mv[0][1]]
if mv: do(mv[0])
while drain and moves(): do(moves()[0]); check() # traffic mode: nothing overlaps
check()
if script is not None: steps = 0 # after a script: just drain, in seeded order
for step in range(10 ** 6):
mv, pending = moves(), to_c or link or hq
if pending and not mv: # the progress check
raise AssertionError(f"deadlock: no enabled move, H busy {bool(H['busy'])}, "
f"buffer {[x['kind'] for x in hq]}, waiting {[x['kind'] for x in link]}")
for w in {"request" if m["kind"] in REQ else "response" for m in link if not room(m)}:
seen[w + " waits for buffer space"] += 1
if step >= steps and not pending: break
if mv and (step >= steps or rng.random() < 0.6): do(mv[rng.randrange(len(mv))])
elif step < steps: op(rng.randrange(n))
check()
assert H["busy"] is None and all(x["st"] in STABLE for x in c), "end: not quiescent"
holders = {i for i in range(n) if c[i]["st"] != "I"}
recorded = H["sh"] if H["dir"] == "S" else {H["own"]} if H["dir"] == "X" else set()
assert holders == recorded, f"end: directory records {recorded}, caches hold {holders}"
dirty = [x["v"] for x in c if x["st"] == "M"]
assert (dirty[0] if dirty else H["mem"]) == gold[0], "end: newest value lost"
except AssertionError as e:
return (str(e) if step is None else f"step {step}: {e}"), cnt
return None, cnt
WARM = [(0, "load"), ("GetS", 0, "H"), ("Data", "H", 0), (1, "load"), ("GetS", 1, "H"),
("Fwd-GetS", "H", 0), ("Data", 0, "H"), ("Data", "H", 1)] # warm-up: C0 S / 0, C1 S / 0
A = WARM + [(1, "store"), (0, "store"), ("GetM", 1, "H", "wait"), ("GetM", 0, "H")]
A2 = WARM + [(1, "store"), (0, "store"), ("GetM", 0, "H", "wait"), ("GetM", 1, "H")]
B0 = [(0, "store"), ("GetM", 0, "H"), ("Data", "H", 0), (0, "evict"), (1, "store")]
B = B0 + [("GetM", 1, "H"), ("Fwd-GetM", "H", 0), ("Data", 0, "H"), ("Data", "H", 1), ("PutM", 0, "H"),
("Put-Ack", "H", 0), (0, "load")]
B2 = B0 + [("PutM", 0, "H"), ("Put-Ack", "H", 0), ("GetM", 1, "H"), ("Data", "H", 1), (0, "load")]
C = WARM + [(0, "store"), ("GetM", 0, "H"), (1, "store"), (2, "load"), ("GetM", 1, "H", "wait"),
("GetS", 2, "H", "wait"), ("Inv", "H", 1)]
SCRIPTS = {"lost-inv": (A, A2), "late-put": (B, B2), "shared-queue": (C,)} # each race, each order
r_first = []
for d, scripts in SCRIPTS.items():
for sc in scripts:
runs = set()
for seed in range(10): # fixed: the race completes on every seed
log = []
assert run(seed, script=sc, log=log)[0] is None, (d, seed)
runs.add(tuple(log))
served = [e for e in log if e in (("GetM", 1, "H"), ("GetS", 2, "H"))]
if sc is C and served[0][0] == "GetS": r_first.append(seed) # H served R before B
assert (len(runs) == 1) == (sc is not C), (d, len(runs)) # A, A2, B, B2: one forced drain order
assert r_first == [0, 2, 3, 4, 5], r_first # C: both orders, B first on 1 and 6 to 9
assert all(run(0, defects=(d,), script=sc)[0] for d, sc in (("lost-inv", A), ("lost-inv", A2),
("late-put", B), ("shared-queue", C))) # failing case: each defect still fails
for size in (1, 2, 3): # request slots
for slots in (1, None): # response slots: 1, or n - 1 = 2
seen = Counter()
for seed in range(20):
fail, _ = run(seed, hq_size=size, resp_size=slots, seen=seen)
assert fail is None, (size, slots, seed, fail)
assert (seen["response waits for buffer space"] > 0) == (slots == 1), (size, slots)
assert "deadlock" in run(0, resp_size=0)[0] # no response slot: the progress check fires
assert run(0, hq_size=1, defects=("shared-queue",))[0] # one shared slot still deadlocks
def turn(w): return [(w, "load"), (w, "store")] + [(r, "load") for r in range(3) if r != w]
counter = [e for _ in range(2) for w in range(3) for e in turn(w)] + [(2, "evict")]
producer = [e for _ in range(3) for e in [(0, "store")] + [(1, "load"), (2, "load")] * 2] + [(0, "evict")]
mesi = {}
for name, tr in (("counter", counter), ("producer", producer)):
fail, cnt = run(0, script=tr, drain=True) # traffic mode: one operation at a time
assert fail is None, (name, fail)
mesi[name] = (cnt["msgs"], cnt["memwr"])
assert mesi == {"counter": (70, 6), "producer": (34, 3)}, mesi
print(f"PASS: the fixed model passes five race scripts on 10 seeds each and 120 seeded runs, MESI traffic {mesi}")Each script fixes its own arrival order at H, so the fixed model is tested once per order: A and A2 for the two winners of the upgrade race, B and B2 for the GetM and the PutM reaching H first, and C. After A, A2, B and B2 the drain is forced: one message can move at each step, so all ten seeds replay the same run. After C both orders occur: H serves R before B on seeds 0, 2, 3, 4 and 5, and B before R on seeds 1 and 6 to 9; both complete. The model also passes 20 seeds at each of three request-buffer sizes with 1 and with 2 response slots: with 1 slot some responses wait for a slot and still sink, with 2 none ever waits, and with none the progress check fires. Each drawn trace still fails with its defect switched back on, so the scripts really reach the race. In traffic mode (each operation drained before the next, so nothing overlaps) the model also counts messages and memory writes. On the producer trace of Comparing Protocol Traffic it gives 34 messages and 3 memory writes for MESI, the same numbers as that lesson's counter: two independent implementations agree.
The capstone trace (illustrative) is a shared counter: in each turn one core reads and then writes X, and the other two read it. Three turns per round (C0, C1, C2), two rounds, then C2 evicts. The counting rules are those of the Part 8 lesson (data through H, acks at H, notified evictions, one message per delivery); MOESI adds the O state, in which a dirty holder answers reads without writing memory. The MESI numbers are the ones the full model produced above.
def count(proto, trace, broken=False):
"""One operation at a time (no races): data through H, acks at H, notified evictions."""
st, val, mem, last, n = {}, {}, 0, 0, {"msgs": 0, "memwr": 0}
for i, op in trace:
s = st.get(i, "I")
others = {k: x for k, x in st.items() if k != i and x != "I"}
holder = next((k for k, x in others.items() if x in "EMO"), None)
if op == "load" and s == "I":
if holder is None: # GetS, Data from memory
n["msgs"] += 2; val[i] = mem; st[i] = "S" if others else "E"
else: # GetS, Fwd-GetS, Data to H, Data
n["msgs"] += 4; val[i] = val[holder]; st[i] = "S"
if proto == "MOESI" and others[holder] in "MO":
st[holder] = "O" if not broken else "S" # broken: dirty S, nobody responsible
else: # E or M holder in MESI, E in MOESI
st[holder] = "S"; mem = val[holder]; n["memwr"] += 1
if op == "load":
assert val[i] == last, f"{proto}: load at C{i} got {val[i]}, last store {last}"
elif op == "store":
if s not in "EM": # E to M is silent
n["msgs"] += 2 + 2 * sum(x == "S" for x in others.values()) # GetM, Data, Inv, Inv-Ack
if holder is not None: n["msgs"] += 2 # Fwd-GetM, Data to H
for k in others: st[k] = "I"
last += 1; st[i], val[i] = "M", last
elif op == "evict" and s != "I":
n["msgs"] += 2 # PutS, PutE or PutM, then Put-Ack
if s in "MO": mem = val[i]; n["memwr"] += 1 # PutM writes memory
st[i] = "I"
if not any(x in "MO" for x in st.values()):
assert mem == last, f"{proto}: memory {mem} after the last dirty copy left, last store {last}"
return n["msgs"], n["memwr"]
def turn(w): return [(w, "load"), (w, "store")] + [(r, "load") for r in range(3) if r != w]
counter = [e for _ in range(2) for w in range(3) for e in turn(w)] + [(2, "evict")]
mesi, moesi = count("MESI", counter), count("MOESI", counter)
assert (mesi, moesi) == ((70, 6), (82, 1)), (mesi, moesi) # MESI numbers equal the full model's
start, steady = turn(0), turn(1) # one steady turn, after C0's turn
d = {p: tuple(a - b for a, b in zip(count(p, start + steady), count(p, start))) for p in ("MESI", "MOESI")}
assert d == {"MESI": (12, 1), "MOESI": (14, 0)}, d # O saves the write, costs 2 messages
private = [(0, "load"), (0, "store"), (0, "store"), (0, "evict")]
assert count("MESI", private) == count("MOESI", private) == (4, 1) # no dirty sharing: no saving
try:
count("MOESI", counter, broken=True)
raise SystemExit("broken MOESI was not caught")
except AssertionError as e:
assert str(e) == "MOESI: load at C2 got 0, last store 1", e
print(f"PASS: counter trace MESI {mesi} vs MOESI {moesi} (messages, memory writes), O saves one write per turn")Counter trace (illustrative) | Messages | Memory writes | Where they come from |
|---|---|---|---|
MESI, first turn (C0) | 8 | 1 | GetS grants E (2), silent store, C1's read forwarded (4, writes memory), C2 from memory (2) |
MESI, each later turn | 12 | 1 | Upgrade from S with two Invs (6), first reader forwarded (4, writes memory), second from memory (2) |
MOESI, first turn | 10 | 0 | As MESI, but C0 goes to O and C2's read is forwarded too (4) |
MOESI, each later turn | 14 | 0 | Upgrade with one Inv plus Fwd-GetM to the O holder (6), both readers forwarded (4 + 4) |
Final eviction | 2 in both | MESI 0, MOESI 1 | MESI: C2 leaves S with PutS. MOESI: C2 leaves O with PutM |
Total, two rounds | MESI 70, MOESI 82 | MESI 6, MOESI 1 |
Where MOESI saves: every time a dirty line is read by another cache. MESI must write memory at that read (H cannot tell E from M, so it writes on every forwarded read); MOESI keeps the data dirty in O and writes once, when the last O holder evicts. On this trace that is one write per turn, five writes saved out of six, paid for with two extra messages per turn because the second reader is served by the O holder through H instead of by memory. Over the whole trace MOESI spends 12 more messages to save 5 writes: 2.4 messages per saved write, 2 in a steady turn. On a private line (load, stores, evict) the two protocols are identical. The broken variant, an M holder that drops to S without writing memory, is caught at C2's first read: memory still holds 0.
If the cache freed its WB entry when it sent PutM, a Fwd-GetM arriving in MI^A (step 2 of trace b) would find no data to answer with. H would then have no current copy anywhere except the in-flight PutM, which it must ignore as late. The cache must keep the WB entry until Put-Ack, and H must ignore Puts from non-holders: the two halves together keep exactly one authoritative copy.
E to M is silent, so after C0's first store H still records exclusive(C0) and cannot know the line is dirty. When C1 reads, H forwards and writes the returned data to memory, as it does for every forwarded read in this model. That is one write in each of the six turns.