Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 102 additions & 25 deletions interactive_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,51 @@
'heatmaps': {}
}

CACHE_CONFIG = {
'cache_size': 32768,
'line_size': 64,
'associativity': 8
}


def compute_cache_stats(tracks, matrix_size):
"""Compute cache statistics and per-frame hit/miss data."""
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parameter documentation: The compute_cache_stats function docstring only provides a brief one-line description but doesn't document the parameters (tracks, matrix_size) or the return value structure. Given that this function returns a dictionary with extended fields (hit_rate_by_frame, hits_by_frame, misses_by_frame) beyond what get_statistics returns, the docstring should document the expected format of tracks and describe all fields in the returned dictionary.

Suggested change
"""Compute cache statistics and per-frame hit/miss data."""
"""
Compute cache statistics and per-frame hit/miss data for GEMM accesses.
Args:
tracks: Iterable of access triples for each frame. Each element is a
tuple ``(a_pos, b_pos, c_pos)`` where:
* ``a_pos`` is a 2-tuple ``(row, col)`` of zero-based integer indices
into matrix A.
* ``b_pos`` is a 2-tuple ``(row, col)`` of zero-based integer indices
into matrix B.
* ``c_pos`` is a 2-tuple ``(row, col)`` of zero-based integer indices
into matrix C.
For each frame, the function simulates one access to A, one access
to B, and two consecutive accesses to C at the given positions.
matrix_size: Integer size of the (square) matrices. This is used to
compute linear offsets from the 2D indices in ``tracks`` when
mapping elements of A, B, and C to memory addresses.
Returns:
dict: A dictionary containing overall cache statistics as returned by
:meth:`CacheSimulator.get_statistics`, extended with the following
additional fields:
* ``hit_rate_by_frame``: list of float
The cumulative cache hit rate after processing each frame, in the
same order as the input ``tracks``.
* ``hits_by_frame``: list of int
The cumulative number of cache hits after each frame.
* ``misses_by_frame``: list of int
The cumulative number of cache misses after each frame.
"""

Copilot uses AI. Check for mistakes.
cache = CacheSimulator(
cache_size=CACHE_CONFIG['cache_size'],
line_size=CACHE_CONFIG['line_size'],
associativity=CACHE_CONFIG['associativity']
)
matrix_bases = {
'A': 0x10000,
'B': 0x20000,
'C': 0x30000
}
Comment on lines +44 to +48
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded matrix_bases dictionary is duplicated between compute_cache_stats (lines 44-48) and CacheSimulator.simulate_accesses (default parameter on lines 135-139 in cache_simulator.py). These should be defined once as a constant to avoid inconsistency if the base addresses need to change.

Copilot uses AI. Check for mistakes.

hit_rate_by_frame = []
hits_by_frame = []
misses_by_frame = []

for a_pos, b_pos, c_pos in tracks:
addr_a = matrix_bases['A'] + (a_pos[0] * matrix_size + a_pos[1]) * cache.element_size
cache.access(addr_a)

addr_b = matrix_bases['B'] + (b_pos[0] * matrix_size + b_pos[1]) * cache.element_size
cache.access(addr_b)

addr_c = matrix_bases['C'] + (c_pos[0] * matrix_size + c_pos[1]) * cache.element_size
Comment on lines +55 to +61
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The address calculation in compute_cache_stats directly uses cache.element_size but doesn't use the _get_address helper method from CacheSimulator. The formula on line 55 (a_pos[0] * matrix_size + a_pos[1]) * cache.element_size is duplicated from CacheSimulator._get_address. This creates a maintenance risk if the address calculation logic needs to change. Consider using CacheSimulator's _get_address method or making it a public method to ensure consistency.

Suggested change
addr_a = matrix_bases['A'] + (a_pos[0] * matrix_size + a_pos[1]) * cache.element_size
cache.access(addr_a)
addr_b = matrix_bases['B'] + (b_pos[0] * matrix_size + b_pos[1]) * cache.element_size
cache.access(addr_b)
addr_c = matrix_bases['C'] + (c_pos[0] * matrix_size + c_pos[1]) * cache.element_size
addr_a = matrix_bases['A'] + cache._get_address(a_pos[0] * matrix_size + a_pos[1])
cache.access(addr_a)
addr_b = matrix_bases['B'] + cache._get_address(b_pos[0] * matrix_size + b_pos[1])
cache.access(addr_b)
addr_c = matrix_bases['C'] + cache._get_address(c_pos[0] * matrix_size + c_pos[1])

Copilot uses AI. Check for mistakes.
cache.access(addr_c)
cache.access(addr_c)

hit_rate_by_frame.append(cache.get_hit_rate())
hits_by_frame.append(cache.hits)
misses_by_frame.append(cache.misses)

stats = cache.get_statistics()
stats['hit_rate_by_frame'] = hit_rate_by_frame
stats['hits_by_frame'] = hits_by_frame
stats['misses_by_frame'] = misses_by_frame
return stats
Comment on lines +37 to +73
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compute_cache_stats function duplicates logic already present in CacheSimulator.simulate_accesses. Both functions iterate through tracks, compute addresses using the same formula, and call cache.access() in the same pattern (A, B, C, C). The main difference is that compute_cache_stats records per-frame statistics. Consider refactoring to avoid this duplication, perhaps by modifying CacheSimulator.simulate_accesses to optionally record per-frame statistics, or by extracting the address calculation into a shared helper function.

Copilot uses AI. Check for mistakes.

# Color scheme for matrices
COLORS = {
'A': 'rgba(255, 0, 0, 0.7)', # Red
Expand Down Expand Up @@ -173,7 +218,6 @@
@app.callback(
[Output('frame-slider', 'max'),
Output('frame-slider', 'marks'),
Output('statistics-panel', 'children'),
Output('animation-state', 'data')],
[Input('matrix-size-slider', 'value'),
Input('block-size-slider', 'value'),
Expand All @@ -188,8 +232,7 @@ def update_simulation(n, block_size, loop_order, blocked, reset_clicks):
tracks = sim.simulate(loop_order, blocked=blocked)

# Run cache simulation
cache = CacheSimulator(cache_size=32768, line_size=64, associativity=8)
cache_stats = cache.simulate_accesses(tracks, matrix_size=n)
cache_stats = compute_cache_stats(tracks, matrix_size=n)

# Store in global state
simulation_data['simulator'] = sim
Expand All @@ -202,26 +245,7 @@ def update_simulation(n, block_size, loop_order, blocked, reset_clicks):
step = max(1, max_frame // 10)
marks = {i: str(i) for i in range(0, max_frame + 1, step)}

# Statistics panel content
stats_content = html.Div([
html.H4("📈 Statistics", style={'color': '#34495e', 'marginBottom': 15}),
html.P([html.Strong("Matrix Size: "), f"{n} × {n}"]),
html.P([html.Strong("Block Size: "), f"{block_size}" if blocked else "Unblocked"]),
html.P([html.Strong("Loop Order: "), loop_order.upper()]),
html.Hr(),
html.P([html.Strong("Total Operations: "), f"{len(tracks):,}"]),
html.P([html.Strong("Memory Accesses: "), f"{cache_stats['total_accesses']:,}"]),
html.Hr(),
html.H5("🎯 Cache Performance", style={'color': '#34495e', 'marginTop': 15}),
html.P([html.Strong("Hit Rate: "),
html.Span(f"{cache_stats['hit_rate']:.2f}%",
style={'color': '#27ae60' if cache_stats['hit_rate'] > 80 else '#e74c3c',
'fontSize': 18, 'fontWeight': 'bold'})]),
html.P([html.Strong("Hits: "), f"{cache_stats['hits']:,}"]),
html.P([html.Strong("Misses: "), f"{cache_stats['misses']:,}"]),
])

return max_frame, marks, stats_content, {'playing': False, 'frame': 0}
return max_frame, marks, {'playing': False, 'frame': 0}


# Callback to handle play/pause
Expand Down Expand Up @@ -293,6 +317,52 @@ def update_frame(n_intervals, slider_value, reset_clicks, state, max_frame, spee
return state['frame'], state


@app.callback(
Output('statistics-panel', 'children'),
[Input('frame-slider', 'value'),
Input('matrix-size-slider', 'value'),
Input('block-size-slider', 'value'),
Input('loop-order-dropdown', 'value'),
Input('blocking-radio', 'value')]
)
def update_statistics_panel(frame, n, block_size, loop_order, blocked):
"""Update the statistics panel based on current frame and configuration."""
tracks = simulation_data.get('tracks', [])
cache_stats = simulation_data.get('cache_stats', {})

Comment on lines +330 to +332
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tie stats panel to fresh simulation data

The statistics panel now reads simulation_data directly, but it is no longer updated by the same callback that recomputes tracks/cache_stats. Because Dash fires callbacks for the same input change in unspecified order, this panel can run before update_simulation and then render the previous cache stats while showing the newly selected matrix size/loop order. That stale state can persist until the user moves the frame slider again, so the panel can misreport cache performance for the current configuration. Consider wiring the stats panel to a store/output from update_simulation (or making it depend on a value that changes only after recomputation) to guarantee it uses the new data.

Useful? React with 👍 / 👎.

if not tracks or not cache_stats:
return html.Div()

frame = min(frame, len(tracks) - 1)
hit_rate_by_frame = cache_stats.get('hit_rate_by_frame', [])
hits_by_frame = cache_stats.get('hits_by_frame', [])
misses_by_frame = cache_stats.get('misses_by_frame', [])

current_hit_rate = hit_rate_by_frame[frame] if hit_rate_by_frame else 0.0
current_hits = hits_by_frame[frame] if hits_by_frame else 0
current_misses = misses_by_frame[frame] if misses_by_frame else 0
Comment on lines +330 to +343
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential state mismatch between callback inputs and global state.

This callback depends on global simulation_data but is triggered by the same inputs that trigger update_simulation. If Dash processes callbacks in parallel or out of order, simulation_data may not yet reflect the new configuration when this callback runs.

Additionally, line 352 uses direct key access cache_stats['total_accesses'] which could raise KeyError if the dictionary structure is unexpected.

🛡️ Suggested defensive access
-    current_hit_rate = hit_rate_by_frame[frame] if hit_rate_by_frame else 0.0
-    current_hits = hits_by_frame[frame] if hits_by_frame else 0
-    current_misses = misses_by_frame[frame] if misses_by_frame else 0
+    current_hit_rate = hit_rate_by_frame[frame] if frame < len(hit_rate_by_frame) else 0.0
+    current_hits = hits_by_frame[frame] if frame < len(hits_by_frame) else 0
+    current_misses = misses_by_frame[frame] if frame < len(misses_by_frame) else 0
-        html.P([html.Strong("Memory Accesses: "), f"{cache_stats['total_accesses']:,}"]),
+        html.P([html.Strong("Memory Accesses: "), f"{cache_stats.get('total_accesses', 0):,}"]),
🤖 Prompt for AI Agents
In `@interactive_viz.py` around lines 330 - 343, This callback reads the global
simulation_data (and cache_stats) while being triggered by the same inputs as
update_simulation, risking stale/mismatched state; refactor the callback to
accept the authoritative simulation payload as a callback Input or dcc.Store
value instead of relying on the global simulation_data (referencing symbols
simulation_data and update_simulation) so it always uses the updated data, and
defensively access cache fields (e.g., cache_stats.get('total_accesses', 0) and
use .get for hit_rate_by_frame/hits_by_frame/misses_by_frame) instead of direct
indexing to avoid KeyError when the dictionary structure is unexpected
(referencing cache_stats, hit_rate_by_frame, hits_by_frame, misses_by_frame and
frame).


Comment on lines +341 to +344
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential index out of bounds error when frame equals len(tracks) - 1. The check on line 336 ensures frame is at most len(tracks) - 1, but the arrays hit_rate_by_frame, hits_by_frame, and misses_by_frame could be shorter than len(tracks) if the compute_cache_stats function encounters an error or if there's any mismatch. Add bounds checking before accessing these arrays by index to prevent IndexError.

Suggested change
current_hit_rate = hit_rate_by_frame[frame] if hit_rate_by_frame else 0.0
current_hits = hits_by_frame[frame] if hits_by_frame else 0
current_misses = misses_by_frame[frame] if misses_by_frame else 0
if hit_rate_by_frame:
hr_index = min(frame, len(hit_rate_by_frame) - 1)
current_hit_rate = hit_rate_by_frame[hr_index]
else:
current_hit_rate = 0.0
if hits_by_frame:
hits_index = min(frame, len(hits_by_frame) - 1)
current_hits = hits_by_frame[hits_index]
else:
current_hits = 0
if misses_by_frame:
misses_index = min(frame, len(misses_by_frame) - 1)
current_misses = misses_by_frame[misses_index]
else:
current_misses = 0

Copilot uses AI. Check for mistakes.
Comment on lines +341 to +344
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent handling of empty hit_rate_by_frame list. When hit_rate_by_frame is empty, line 341 sets current_hit_rate to 0.0, but if hit_rate_by_frame exists but frame is beyond its length, an IndexError will be raised. The safer pattern would be to check both if the list exists AND if frame is within bounds before accessing, similar to how hit_rate_x is handled in the visualization callback on lines 431-436.

Suggested change
current_hit_rate = hit_rate_by_frame[frame] if hit_rate_by_frame else 0.0
current_hits = hits_by_frame[frame] if hits_by_frame else 0
current_misses = misses_by_frame[frame] if misses_by_frame else 0
if hit_rate_by_frame and 0 <= frame < len(hit_rate_by_frame):
current_hit_rate = hit_rate_by_frame[frame]
else:
current_hit_rate = 0.0
if hits_by_frame and 0 <= frame < len(hits_by_frame):
current_hits = hits_by_frame[frame]
else:
current_hits = 0
if misses_by_frame and 0 <= frame < len(misses_by_frame):
current_misses = misses_by_frame[frame]
else:
current_misses = 0

Copilot uses AI. Check for mistakes.
stats_content = html.Div([
html.H4("📈 Statistics", style={'color': '#34495e', 'marginBottom': 15}),
html.P([html.Strong("Matrix Size: "), f"{n} × {n}"]),
html.P([html.Strong("Block Size: "), f"{block_size}" if blocked else "Unblocked"]),
html.P([html.Strong("Loop Order: "), loop_order.upper()]),
html.Hr(),
html.P([html.Strong("Total Operations: "), f"{len(tracks):,}"]),
html.P([html.Strong("Memory Accesses: "), f"{cache_stats['total_accesses']:,}"]),
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential KeyError when accessing cache_stats['total_accesses']. If the cache_stats dictionary is missing the 'total_accesses' key (for example, if compute_cache_stats partially fails or if get_statistics() doesn't include it), this line will raise a KeyError. Use cache_stats.get('total_accesses', 0) for safer access.

Suggested change
html.P([html.Strong("Memory Accesses: "), f"{cache_stats['total_accesses']:,}"]),
html.P([html.Strong("Memory Accesses: "), f"{cache_stats.get('total_accesses', 0):,}"]),

Copilot uses AI. Check for mistakes.
html.Hr(),
html.H5("🎯 Cache Performance (to current frame)", style={'color': '#34495e', 'marginTop': 15}),
html.P([html.Strong("Hit Rate: "),
html.Span(f"{current_hit_rate:.2f}%",
style={'color': '#27ae60' if current_hit_rate > 80 else '#e74c3c',
'fontSize': 18, 'fontWeight': 'bold'})]),
html.P([html.Strong("Hits: "), f"{current_hits:,}"]),
html.P([html.Strong("Misses: "), f"{current_misses:,}"]),
])

return stats_content


# Callback to update visualizations
@app.callback(
[Output('main-animation', 'figure'),
Expand Down Expand Up @@ -357,9 +427,16 @@ def update_visualizations(frame, n):
])

# Cache performance chart
hit_rate_history = cache_stats.get('hit_rate_history', [])
hit_rate_history = cache_stats.get('hit_rate_by_frame', [])
if hit_rate_history:
step = max(1, len(hit_rate_history) // 500)
hit_rate_history = hit_rate_history[::step]
hit_rate_x = list(range(0, len(cache_stats.get('hit_rate_by_frame', [])), step))
else:
hit_rate_x = []
cache_fig = go.Figure()
cache_fig.add_trace(go.Scatter(
x=hit_rate_x,
Comment on lines +434 to +439
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear variable naming: The variable hit_rate_x is constructed to represent frame indices, but the name suggests it's related to hit rate values rather than frame numbers. Consider renaming to frame_indices or sampled_frame_indices to clarify that this represents the x-axis (frame numbers) for the plot, not hit rate data.

Suggested change
hit_rate_x = list(range(0, len(cache_stats.get('hit_rate_by_frame', [])), step))
else:
hit_rate_x = []
cache_fig = go.Figure()
cache_fig.add_trace(go.Scatter(
x=hit_rate_x,
sampled_frame_indices = list(range(0, len(cache_stats.get('hit_rate_by_frame', [])), step))
else:
sampled_frame_indices = []
cache_fig = go.Figure()
cache_fig.add_trace(go.Scatter(
x=sampled_frame_indices,

Copilot uses AI. Check for mistakes.
y=hit_rate_history,
mode='lines',
line=dict(color='#27ae60', width=2),
Expand All @@ -368,7 +445,7 @@ def update_visualizations(frame, n):
))
cache_fig.update_layout(
title="Cache Hit Rate Over Time",
xaxis_title="Access (×100)",
xaxis_title="Frame",
yaxis_title="Hit Rate (%)",
yaxis=dict(range=[0, 100]),
height=250,
Expand Down
Loading