Agent tools (MCP)

TT-NN Visualizer ships a Model Context Protocol server so a coding agent can ask a report the questions a person asks it during model bring-up: which operations dominate, where the time went inside them, whether a change helped, and what the run allocated.

It is a read-only surface over the same report readers the web application uses. It starts no web server, opens no port, and writes nothing to a report.

Running it

ttnn-visualizer-mcp

The server speaks newline-delimited JSON-RPC on stdin and stdout, which is what an MCP client expects from a stdio server. Register it with your client the way you would any other stdio MCP server — for Claude Code:

claude mcp add ttnn-visualizer -- ttnn-visualizer-mcp

Reports are addressed by path, not by the instanceId the web application uses, so the server needs no database and no running application.

The tools

Tool

Answers

load_report

What this report contains, and which of the tools below apply to it. Returns a handle the others take.

top_ops

The costliest operations by device time, op-to-op gap, total percentage, FLOPS, DRAM bandwidth or core count.

zone_timings

Per-zone, per-RISC totals from profile_log_device.csv — firmware and kernel phases, as measured on device.

diff_reports

Per-operation-code deltas between two reports, largest movement first.

find_operations

Operations matching a name substring, with the ids operation_detail takes.

operation_detail

One operation: its input and output tensors with shape, dtype, layout and memory config, and what it had allocated.

memory_profile

Memory footprint per operation, keyed by buffer type and ranked within each, with each type’s peak and the device’s L1 geometry.

tensor_flow

Which operation produced a tensor and which ones consumed it.

The last four read the profiler report’s SQLite database and the middle three read the performance CSVs; load_report opens both, since its whole job is saying which of the others apply. A capture can carry either, both, or — as far as these tools are concerned — neither, which is what makes load_report’s answer worth reading first.

Call load_report first. It reports what is answerable rather than making you discover it one failed call at a time, because the report kinds are independent: a performance-only capture has no operation graph and no tensor data, and a report with no device profiler log cannot answer zone_timings. Each database tool is checked against the tables it actually reads, so a truncated capture that holds operations but not buffers is reported as answering find_operations and nothing else, rather than advertising four tools and failing three of them.

What the answers mean

Five properties are deliberate, and worth knowing before you act on a number.

Every tool returns an aggregate or a bounded slice, never the table. A performance report runs to tens of thousands of rows of about thirty-five fields. Limits are capped server-side, so asking for more returns the cap rather than the report.

No rows are hidden from you. The web application’s performance view hides host operations by default and can narrow to a signpost range. Those are display choices, and an agent handed them unannounced would reason confidently about a partial set. The tools read the report with host operations included, signpost markers kept, and no signpost range.

One filter is applied, deliberately: per-device rows are merged, so an operation that ran on eight devices is one row rather than eight. That is the unit an operation is reported in rather than a subset of the report — but it is a choice, so every response repeats the projection it used and you can see it.

Allocation figures are per bank, and nothing adds one memory type to another. memory_profile and the allocations in operation_detail come from the report’s max_size_per_bank column. That column is divided by the bank count of its own memory type, and those counts differ — so a DRAM figure plus an L1 figure is two denominators in one integer rather than a quantity. memory_profile therefore keys everything by buffer type and ranks operations within a type, never across types.

A per-bank L1 figure is comparable to the l1_bank_size returned beside it, which is a conservative bound. A device-wide total cannot be derived from the response, and the response says so rather than offering a formula: multiplying by the bank count is only right for a buffer interleaved across every bank, and most are not — on a local resnet50 capture the operation holding the L1 peak uses 56 of 64 banks, so multiplying overstates it by 14%, and 16-bank operations in the same report by 4x. How many banks a buffer actually occupies lives in page-level data that no tool exposes. The report carries no DRAM capacity at all, which is likewise stated rather than left as a gap.

What operation_detail does give you is each tensor’s memory_config, whose memory_layout says whether a tensor is interleaved or sharded, and whose shard_spec carries the grid and shape when it is — so you can tell why a per-bank figure does not scale by the device’s bank count, rather than only being told that it doesn’t.

A tensor’s size is a whole-tensor byte count — where the report carries that column. Where it does not, which is the common case, the report’s own query substitutes the per-bank allocation figure, and the response labels it bytes_per_bank accordingly. So read tensor_size_unit rather than assuming the two figures are in different units.

memory_profile groups by operation because the report records what was live at each operation rather than what that operation allocated. A per-operation sum is therefore the footprint at that point in the run, and the largest of them is that type’s peak — which is the question an out-of-memory failure asks. A peak is a maximum and never a sum across the run: buffers persist across operations, so adding them would count one allocation once per operation it stayed live through. Because resident memory barely moves, a peak is usually shared by many operations, and operations_at_peak says how many — the difference between a single operation you can go and fix and a plateau across the whole run.

A multi-host report is read one rank at a time, and refuses rather than guess. Operation ids restart at 1 per rank, so reading every rank at once would collide operations that merely share an id. The database tools default to rank 0, take a rank argument, and name the rank they read in every response along with a caveat saying the figures describe that rank rather than the job. A rank the report has no rows for is refused rather than answered empty, since “no operations at rank 9” otherwise reads as a fact about the run. And where a report carries rank on its operations but not on the table holding the figures — a schema mix that would silently union every rank and attribute the total to whichever rank you asked for — the call is refused instead, because a caveat naming a rank the numbers do not describe is worse than no answer.

A total on a partitioned run carries a caveat. When a report spans more than one sub-device, operations on different sub-devices can run concurrently, so summed device time, total percentages and op-to-op gaps overstate elapsed time. Responses that include a total say so, and name the sub-devices involved — top_ops and diff_reports alike, since a delta between two unsound totals is unsound the same way.

Totals are only reported for metrics a sum means something for: device time, op-to-op gap and total percentage. DRAM bandwidth, FLOPS and core count are per-operation figures, and adding them across a report would produce a number that looks authoritative and is not.

zone_timings carries a caveat of its own: cycles are summed across every core that ran the zone, so they measure occupancy rather than wall-clock duration. A core is counted per device — the captures we test against span 8 and 32 PCIe slots, and coordinates alone repeat on each — so a zone can report thousands of cores on a multi-device run. Durations need the device log’s type column to pair zone starts with ends; a capture without it reports occurrence counts only, and a capture that stopped mid-zone reports how many starts and ends failed to pair so a partial total does not read as a complete one.

Limitations

Page-level memory questions — fragmentation, or the per-bank detail behind the web application’s memory plot — are not exposed. That data runs to millions of rows on an ordinary capture and needs a different shape than a tool response.

Device profiler logs carry named zones only where a kernel was instrumented to emit them. Most captures contain only the default firmware and kernel zones — BRISC-FW, BRISC-KERNEL and the same pair for NCRISC and TRISC, plus ERISC where a capture used ethernet cores — so on those reports zone_timings describes RISC-level phases rather than named model operations. A capture whose kernels do emit named zones reports them alongside the defaults.

The transport is a minimal JSON-RPC implementation rather than the official MCP SDK, which keeps the server free of additional dependencies while the tool set settles.