Files
Raptor/test/PIM/test_PimPeriodicDiagnostic.py
T

338 lines
20 KiB
Python

import argparse
import copy
import hashlib
import json
from pathlib import Path
import re
import struct
import subprocess
import tempfile
def earliest_events(report):
graph = report['timing_graph']
incoming = [0] * len(graph['events'])
outgoing = [[] for _ in incoming]
for edge in graph['dependencies']:
if edge['distance'] == 0:
incoming[edge['target']] += 1
outgoing[edge['source']].append(edge)
ready = [i for i, count in enumerate(incoming) if count == 0]
times = [0] * len(incoming)
for source in ready:
for edge in outgoing[source]:
target = edge['target']
times[target] = max(times[target], times[source] + edge['delay_ns'])
incoming[target] -= 1
if incoming[target] == 0:
ready.append(target)
assert len(ready) == len(times), 'within-generation graph is cyclic'
return graph, times
def record(op, arg=0, a=0, b=0, length=0):
return struct.pack('<BBBBiiii', op, 0, 0, 0, arg, a, b, length)
def check_controller(run):
scalar = record(1)
load = lambda n: record(25, length=n)
store = lambda n: record(26, length=n)
work = record(27, length=256)
barrier = lambda peer: [record(32, arg=peer, a=0), record(31, a=0, b=1)]
staggered = [barrier(1) + [load(64), work], barrier(0) + [work, load(64)]]
burst = [barrier(1) + [work, load(64)], barrier(0) + [work, load(64)]]
a = run(staggered, controller=True, issue=True)
b = run(burst, controller=True, issue=True)
for report in (a, b):
assert report['memory_controller']['status'] == 'estimated'
assert report['global_read_bytes'] == 128
assert report['global_memory_service_ns'] == 160
assert a['memory_controller']['interval_ns'] < b['memory_controller']['interval_ns']
assert b['memory_controller'] == run(burst, controller=True, issue=True)['memory_controller']
mixed = run([[scalar, load(33), scalar], [scalar, store(128), scalar]],
controller=True, issue=True)
queued = mixed['memory_controller']
assert mixed['global_memory_service_ns'] == 280
assert queued['interval_ns'] >= 280
assert queued['completed_generations'] >= 4
assert any(core['queue_wait_ns'] > 0 for core in queued['per_core'])
sizes = run([[scalar, load(1), scalar], [scalar, load(65), scalar]], controller=True, issue=True)
assert sizes['global_memory_service_ns'] == 160
assert sizes['memory_controller']['interval_ns'] >= 160
delayed = [barrier(1) + [load(8192)], barrier(0) + [load(64)]]
behind = run(delayed, controller=True, issue=True)
assert behind['memory_controller']['status'] == 'estimated'
reader = next(core for core in behind['memory_controller']['per_core'] if core['core'] == 1)
assert reader['queue_wait_ns'] > 0
zero = run([barrier(1) + [record(27, length=128), load(0)], barrier(0) + [store(128)]],
controller=True, issue=True)
reader = next(core for core in zero['memory_controller']['per_core'] if core['core'] == 0)
assert reader['queue_wait_ns'] > 0 # Zero service still queues behind an active request.
bounded = json.loads(run([[scalar, scalar], [scalar, load(65536), scalar]],
controller=True, issue=True, valid=False).stdout)
assert bounded['status'] == 'unavailable'
assert bounded['memory_controller']['status'] == 'unavailable'
assert 'periodic_state' not in bounded['memory_controller']
shared = [[record(31, a=0, b=2), store(128), record(32, arg=1, a=1), record(32, arg=2, a=1)],
[record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(29, arg=2, length=64)],
[record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(30, arg=1, length=64)]]
publication = run(shared, controller=True, issue=True)
assert publication['memory_controller']['status'] == 'estimated'
assert publication['global_write_requests'] == 1 and publication['global_read_requests'] == 2
assert publication['direct_requests'] == 1
return [staggered, burst, [[scalar, load(33), scalar], [scalar, store(128), scalar]],
[[scalar, load(1), scalar], [scalar, load(65), scalar]], delayed, shared]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--diagnostic', required=True)
parser.add_argument('--pimsim', help='Optional ChipTest executable for matched fast-mode timing checks')
args = parser.parse_args()
repo = Path(__file__).resolve().parents[2]
with tempfile.TemporaryDirectory(prefix='raptor-periodic-isa-') as temporary:
root = Path(temporary)
for arch in ('arch-a', 'arch-b'):
config = json.loads((repo / 'validation/pimsim_configs/pimcomp' / arch /
'throughput_config_1000ms.json').read_text())
config['chip_config']['core_cnt'] = 3
latency = {str(i): {str(j): int(i != j) for j in range(3)} for i in range(3)}
for i in range(3):
latency[str(i)]['-10'] = 2
latency['-10'] = {str(i): 2 for i in range(3)}
network = root / 'network.json'
network.write_text(json.dumps(dict(latency=latency)))
config['chip_config']['network_config']['net_config_file_path'] = str(network)
path = root / 'hardware.json'
path.write_text(json.dumps(config))
hardware = config['chip_config']['core_config']
command = [args.diagnostic, str(root), f'--pim-target-config={path}',
'--core-count=3', '--crossbar-size=128',
f'--crossbar-count={hardware["matrix_config"]["xbar_array_count"]}']
def run(streams, *, issue=False, queues=True, valid=True, controller=False, oracle=False):
for artifact in root.glob('core_*.pim'):
artifact.unlink()
(root / 'config.json').write_text(json.dumps(dict(core_cnt=len(streams),
array_group_map={f'core{i}': [i + 1] for i in range(len(streams))})))
for i, instructions in enumerate(streams):
(root / f'core_{i}.pim').write_bytes(struct.pack('<4sII', b'PIMB', 1,
len(instructions)) + b''.join(instructions))
before = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in root.glob('core_*.pim')}
result = subprocess.run(command + [f'--model-issue={int(issue)}',
f'--model-network-queues={int(queues)}',
f'--model-memory-controller={int(controller)}', f'--oracle-precision={int(oracle)}'], capture_output=True, text=True)
assert (result.returncode == 0) == valid, result.stdout + result.stderr
assert before == {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in root.glob('core_*.pim')}
if not valid:
return result
report = json.loads(result.stdout)
assert report['diagnostic_only'] and not report['selection_eligible']
assert report['status'] == 'estimated'
if report['recurrence_lower_ns'] > 1:
ratio = report['critical_cycle_delay_ns'] / report['critical_cycle_distance']
assert report['recurrence_lower_ns'] <= ratio <= report['recurrence_upper_ns']
return report
bw = record(8, a=8, b=8)
load = lambda n: record(25, length=n)
store = lambda n: record(26, length=n)
send = lambda peer: record(29, arg=peer, length=64)
recv = lambda peer: record(30, arg=peer, length=64)
sync = lambda peer, reg: record(32, arg=peer, a=reg)
wait = lambda reg, n=1: record(31, a=reg, b=n)
controller_fixtures = check_controller(run)
native = run([[record(8, a=32, b=32), load(132), record(10, length=65), store(132)]],
issue=True, controller=True, oracle=True)
exported = run([[bw, load(33), record(10, length=65), store(33)]], issue=True, controller=True)
for key in ('global_read_bytes', 'global_write_bytes', 'global_read_requests',
'global_write_requests', 'global_memory_service_ns'):
assert native[key] == exported[key], key
assert native['memory_controller'] == exported['memory_controller']
run([[record(8, a=16, b=16)]], oracle=True, valid=False)
run([[load(3)]], oracle=True, valid=False)
# One scan counts input, constant, and final-output accesses exactly.
memory = run([[load(33), load(33), store(33)]])
assert memory['global_read_requests'] == 2
assert memory['global_read_bytes'] == 66 and memory['global_write_bytes'] == 33
assert memory['global_memory_service_ns'] == 260
assert memory['per_core'][0]['serial_work_ns'] == 348
folded = run([[load(66), store(33)]])
assert folded['global_memory_service_ns'] == 220
scalar = run([[record(1)] * 10], issue=True)
assert scalar['per_core'][0]['serial_work_ns'] == 10 * hardware['period']
vector = run([[bw, record(10, length=65)]])
vector_cost = 2 * 2 * 20 + 3 * 4 + 2 * 30
assert vector['per_core'][0]['resource_service_ns'][1] == vector_cost
issued = run([[bw, record(10, length=65)]], issue=True)
assert issued['per_core'][0]['serial_work_ns'] == vector_cost + 2 * hardware['period']
repeated = run([[bw] + [record(10, length=65)] * 10000])
assert repeated['per_core'][0]['serial_work_ns'] == 10000 * vector_cost
assert repeated['events'] < 10
matrix = run([[bw, record(9, arg=2)], [bw, record(9, arg=2)]])
assert len(matrix['per_core']) == 2
assert matrix['per_core'][0]['instructions'] == matrix['per_core'][1]['instructions'] == 2
assert matrix['per_core'][0]['resource_service_ns'][0] > 0
assert matrix['per_core'][1]['serial_work_ns'] - matrix['per_core'][0]['serial_work_ns'] == 60
# Equal compute and traffic, different placement of a baseline load.
reader = [bw, recv(0), record(9, arg=32), load(1024), send(0)]
overlap = run([[send(1), load(1024), recv(1)], reader])
serial = run([[load(1024), send(1), recv(1)], reader])
assert overlap['global_memory_service_ns'] == serial['global_memory_service_ns']
assert overlap['interval_estimate_ns'] < serial['interval_estimate_ns']
output = run([[send(1)], [recv(0), store(64)]])
no_output = run([[send(1)], [recv(0)]])
assert output['global_write_requests'] == 1 and output['global_write_bytes'] == 64
assert output['direct_requests'] == 1 and output['direct_bytes'] == 64
assert output['interval_estimate_ns'] > no_output['interval_estimate_ns']
# Permission precedes the complete source read; enqueue is immediate.
delayed = run([[send(1)], [record(27, length=128), recv(0)]])
graph, times = earliest_events(delayed)
def event_id(core, pc, kind, occurrence=0):
return [i for i, event in enumerate(graph['events'])
if event == dict(core=core, pc=pc, kind=kind)][occurrence]
request = event_id(0, 0, 'delivery', 0)
permission = event_id(1, 1, 'delivery')
enqueue = event_id(0, 0, 'payload-enqueue')
delivery = event_id(0, 0, 'delivery', 1)
complete = event_id(0, 0, 'send-complete')
received = event_id(1, 1, 'receive-complete')
assert times[permission] >= max(times[request], 60) + 1
assert times[enqueue] == times[permission] + 20
assert times[complete] == times[enqueue]
assert times[delivery] == times[enqueue] + 2
assert times[received] == times[delivery] + 30
assert any(edge['source'] == delivery and edge['distance'] == 1
for edge in graph['dependencies'])
# An occupied source FIFO delays request dispatch and permission.
busy_routes = copy.deepcopy(latency)
busy_routes['0']['2'] = 100
network.write_text(json.dumps(dict(latency=busy_routes)))
graph, times = earliest_events(run([[sync(2, 0), send(1)], [recv(0)], [wait(0)]]))
dispatch = event_id(0, 1, 'fifo-dispatch', 0)
enqueue = event_id(0, 1, 'payload-enqueue')
complete = event_id(0, 1, 'send-complete')
assert times[dispatch] == 100
assert times[enqueue] == times[complete] == 122
network.write_text(json.dumps(dict(latency=latency)))
pairs = run([[send(1), send(1)], [recv(0), recv(0)]])
assert pairs['direct_requests'] == 2 and pairs['direct_bytes'] == 128
interleaved = run([[wait(0), send(1), sync(1, 1)],
[sync(0, 0), recv(0), wait(1)]])
assert interleaved['direct_requests'] == 1
# Exact-count handshakes repeat safely, including shared storage reuse.
shared = run([[wait(0, 2), store(128), sync(1, 1), sync(2, 1)],
[sync(0, 0), wait(1), load(128)],
[sync(0, 0), wait(1), load(128)]])
assert shared['global_write_requests'] == 1 and shared['global_read_requests'] == 2
assert shared['global_write_bytes'] == 128 and shared['global_read_bytes'] == 256
stalled = run([[wait(0, 2), store(128), sync(1, 1), sync(2, 1)],
[sync(0, 0), wait(1), load(128)],
[bw, sync(0, 0), wait(1), load(128), record(10, length=4096)]])
assert stalled['interval_estimate_ns'] > shared['interval_estimate_ns']
notices = [[sync(1, 0), sync(1, 0), wait(1)], [wait(0, 2), sync(0, 1)]]
queued = run(notices)
unqueued = run(notices, queues=False)
assert queued['interval_estimate_ns'] >= unqueued['interval_estimate_ns']
# Delivery starts when SYNC enqueues, before its local completion.
slower_clock = copy.deepcopy(config)
slower_clock['chip_config']['core_config']['period'] = 8
slower_clock['chip_config']['core_config']['local_memory_config'].pop('period')
path.write_text(json.dumps(slower_clock))
signal = run([[sync(1, 0), wait(1)], [wait(0), sync(0, 1)]])
assert signal['interval_estimate_ns'] < 18
local = run([[record(27, length=65)]])
assert local['per_core'][0]['serial_work_ns'] == 60
independent_vector = run([[bw, record(10, length=65)]])
assert independent_vector['per_core'][0]['serial_work_ns'] == vector_cost
different_memory_clock = copy.deepcopy(slower_clock)
different_memory_clock['chip_config']['core_config']['local_memory_config']['period'] = 3
different_memory_clock['chip_config']['core_config']['vector_latency_cycle'] = 7
path.write_text(json.dumps(different_memory_clock))
changed_vector = run([[bw, record(10, length=65)]])
assert changed_vector['per_core'][0]['serial_work_ns'] == (vector_cost - 12) * 3 + 21
path.write_text(json.dumps(config))
zero_service = copy.deepcopy(config)
zero_service['chip_config']['global_memory_config']['read_latency_cycle'] = 0
zero_routes = copy.deepcopy(latency)
zero_routes['0']['-10'] = 0
zero_routes['0']['1'] = 100
network.write_text(json.dumps(dict(latency=zero_routes)))
path.write_text(json.dumps(zero_service))
prefix = [[load(0), sync(1, 0), load(64), wait(1)], [wait(0), sync(0, 1)]]
drained = run(prefix)
undrained = run(prefix, queues=False)
assert drained['interval_estimate_ns'] > undrained['interval_estimate_ns'] + 20
network.write_text(json.dumps(dict(latency=latency)))
path.write_text(json.dumps(config))
if args.pimsim:
# Scalar padding avoids the oracle's single-instruction fetch loop.
# Golden pairs are (rounded graph estimate, oracle interval), in ns.
# Their difference exposes clock-phase/frontend costs, without fitting the model.
fixtures = [([[send(1)], [recv(0)]], {}, {}, (56, 57)),
([[send(1)], [record(27, length=1024), recv(0)]], {}, {}, (537, 539)),
([[send(1)], [recv(0)]], {'memory_period': 3}, {}, (157, 157)),
([[send(1)], [recv(0)]], {}, {'peer': 100}, (322, 321)),
([[sync(2, 0), send(1)], [recv(0)], [wait(0)]], {}, {'third': 100}, (125, 126)),
([[send(1), send(1)], [recv(0), recv(0)]], {}, {}, (111, 112)),
([[wait(0), send(1), sync(1, 1)], [sync(0, 0), recv(0), wait(1)]], {}, {}, (60, 65)),
([[bw, record(10, length=65)]], {'period': 8, 'memory_period': 3, 'vector': 7}, {}, (473, 488)),
([[bw, record(10, length=65)]], {'period': 5, 'memory_period': 2, 'vector': 11}, {}, (333, 340))]
fixtures = [(*fixture, False) for fixture in fixtures]
controller_expected = [(237, 243), (316, 320), (281, 280), (185, 186), (14145, 14169), (690, 698)]
assert len(controller_fixtures) == len(controller_expected)
fixtures += [(streams, {}, {}, expected, True) for streams, expected in
zip(controller_fixtures, controller_expected)]
for streams, clocks, links, expected, controller in fixtures:
matched = copy.deepcopy(config)
core = matched['chip_config']['core_config']
core['period'] = clocks.get('period', 1)
core['local_memory_config']['period'] = clocks.get('memory_period', 1)
core['vector_latency_cycle'] = clocks.get('vector', 4)
matched['sim_config']['sim_time'] = 1
path.write_text(json.dumps(matched))
routes = copy.deepcopy(latency)
routes['0']['1'] = links.get('peer', 1)
routes['0']['2'] = links.get('third', 1)
network.write_text(json.dumps(dict(latency=routes,
energy={s: {d: 0 for d in row} for s, row in routes.items()})))
program = streams if controller else [[record(1)] + stream + [record(1)] for stream in streams]
prediction = run(program, issue=True, controller=controller)
result = subprocess.run([args.pimsim, str(root), str(path), '--gui=false', '--fast'],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
assert 'Fast throughput converged' in result.stdout
measured = float(re.search(r'average latency:\s+(\S+)', result.stdout)[1]) * 1e6
estimate = prediction['memory_controller']['interval_ns'] if controller else prediction['interval_estimate_ns']
assert (round(estimate), round(measured)) == expected
path.write_text(json.dumps(config))
network.write_text(json.dumps(dict(latency=latency)))
run([[send(1), recv(1)], [send(0), recv(0)]], valid=False)
run([[sync(1, 0)], [wait(0, 2)]], valid=False)
run([[record(21, length=1)]], valid=False)
run([[]], valid=False)
unsupported = copy.deepcopy(config)
unsupported['chip_config']['core_config']['rob_size'] = 2
path.write_text(json.dumps(unsupported))
run([[load(33)]], valid=False)
if __name__ == '__main__':
main()