Model summaries¶
tl.summary(model, x) is the one-call door: it runs ONE metadata-only
capture (eval + no_grad by default, every training flag and RNG stream
restored bit-identically), resolves the automatic view ladder, and RETURNS
a summary result. It never auto-prints; the result displays itself in a
REPL or notebook. trace.summary() renders the same report from an
existing capture without re-running anything.
All names on this page are experimental and may change in any release; the accounting laws (identity partition, raw-numbers pin, ASCII contract) are stable.
The sixty-second tour¶
import torch, torchlens as tl
from torchvision import models
report = tl.summary(models.resnet50(weights=None), torch.randn(1, 3, 224, 224))
report.print() # auto-detected charset at YOUR terminal
print(report) # the canonical byte-stable ASCII form (logs, files)
report.total_params # 25557032 -- a plain int, never a formatter object
report.to_pandas() # typed DataFrame of the rendered rows
The default view is an automatic ladder under a 48-body-row budget (48 is derived, not chosen: the smallest budget that renders the entire VGG family at complete op level):
- Coalesced hybrid, full depth -- every executed op appears exactly
once; a module owning one op coalesces into a single
fc1 (Linear)row; orphan functional ops (the classic torchsummary blind spot, e.g. torchvision'storch.flatten) get their own rows. - Module tree, full depth, strictly folded (
h.0..11 (GPT2Block x12)). - Module tree, descending depth.
- Elision: protected, totals-conserving accounting rows replace the lowest-share runs -- never truncation, so no model at any budget collapses to opaque container rows.
The disclosure line always states what resolved
(view: module tree, depth 2 of 4, 4 folds). On gpt2 the default renders
the COMPLETE folded tree in 19 body rows -- nothing hidden.
The numbers are the true ones¶
- Params use torch's own Parameter-object identity:
report.total_params == sum(p.numel() for p in model.parameters())by construction. Tied weights (gpt2'swte/lm_head) are counted once, DISCLOSED by name, and the per-module-path total prints beside the headline when the two differ -- so numbers are comparable against tools that double-count the tie. - FLOPs come from the per-op two-term compute record. Alias (input/output) rows own nothing. Unknown ops stay unknown, are named with a remedy, and make totals explicit lower bounds.
- MACs are true multiply-accumulates (a ReLU has zero; a biased
Linear's bias adds are FLOPs, not MACs) -- never
flops // 2. - Declared-but-never-executed parameters are split out and named
(
3,326,696 never ran: AuxLogits.*on eval-mode inception_v3). - Every parameter identity and executed op event is owned by exactly one accounting row at every depth, fold, filter, and elision (the identity partition -- CI-enforced).
The grammar¶
tl.summary(model, inputs=None, input_kwargs=None,
# Everything below is keyword-only.
input_shape=None, # synthetic input shape (XOR real inputs)
execution_mode="eval", # eval | train | same (one-call only)
grad_mode="off", # off | same (one-call only)
level="auto", # auto | module | op
view="overview", # overview | compute | stats (column bundles; stats = per-site payload statistics)
depth="auto", # auto | all | int
columns=None, # bundle name, exact list, or +name/-name deltas
where=None, # regex, selector or callable; totals stay whole-model
buffers="summary", # summary | hide
fold_repeats="auto", max_rows=48,
flop_convention="fma2", # fma2 | fma1 (refuses typed if underivable)
units="human", # human | raw
style="auto", # auto | ascii | unicode (display helpers only)
)
Granularity, presentation, selection, numbers, and output are orthogonal
axes. Contradictory axes (level="op" with a depth= cut) raise
summary_option_conflict. Nothing is ever accepted-and-ignored.
The legacy spellings are removed (no aliases). Each refuses typed and names its successor:
| Removed spelling | Use instead |
|---|---|
preset= |
view= ("overview" / "compute" / "stats") for columns, level= for row grain |
fields= |
columns= |
show_ops=, include_ops= |
level="op" |
mode= |
level="op", or fold_repeats=False to unfold repeated runs |
print_to= |
report.print(file=...), or print_to(str(report)) |
count_fma_as_two= |
flop_convention="fma2" (was True) / "fma1" (was False) |
show_input_preprocessing_details= |
trace.provenance.input_preprocessor; the capture-provenance text block is trace.provenance.summary() |
level="overview" |
view="overview" (the default) |
level="compute", level="cost" |
view="compute" |
level="graph" |
trace.to_agent_json() (op rows, edges, hierarchy) or trace.draw() |
level="memory" |
trace.profile(sort_by="activation_memory"); totals stay in the footer |
level="control_flow" |
trace.graph.conditional_records and the conditional_* columns of trace.to_pandas() |
level="waterfall" |
trace.profile(level="op"), or trace.to_pandas() in execution order |
level="output" |
trace.output_table() |
The stats view¶
trace.summary(view="stats") is the per-site payload statistics table: one row per
executed op (level="module" keeps the module-output payloads), read in graph order, with
typed row states (ok / unsaved / disk_backed / unsupported / unscanned) so an
absent number is a state, never a zero. max_scan_elements= is the scan budget (accepted only
with view="stats"; later retained rows become unscanned and the header says how many).
Sampled families print with a ~ mark and never a clearance; = parent marks come only from
the corroborated distribution verdict and are never folded. str(report) is the table,
report.rows the typed rows, report.to_pandas() the frame; columns= takes the stats
column names (name, state, n, mean, sd, min, max, zero_count, nan_count,
inf_count, same_as_parent, reason) or +name / -name deltas.
Columns¶
Percentage cells use numbers without glyph bars. A single footer explains any
markers present: * for tied weights, ^ for SDPA full-square FLOPs, and ×N
for repeated modules, passes, or output shapes.
columns= takes a bundle name ("overview", "compute", "stats"), an exact
ordered list, or +name / -name deltas on the view's bundle. A +name
lands at its registry position (+input before output, +flops_pct
beside fwd flops).
| Column | Header | Cell |
|---|---|---|
name |
name (type) |
attribute name and class, indented by depth; [never ran], [pruned], and * (tied) tags |
input |
input |
shapes of the distinct tensors entering the row |
output |
output |
every member shape of the row's output in return order (an LSTM row shows output, h_n, c_n); past 60 characters identical runs print once as shape ×N (a GPT-2 row with a key/value cache shows (1, 5, 768), (1, 12, 5, 64) ×24), and a list still too long ends in ... +K more |
kernel_size |
kernel |
kernel size of the row's own conv or pooling op |
groups |
groups |
conv groups of the row's own conv op |
params |
params |
parameters per module path |
params_pct |
(%) |
share of the declared parameter total |
params_trainable |
trainable |
trainable parameters per module path (a count) |
flops |
fwd flops |
forward FLOPs under flop_convention |
flops_pct |
(%) |
share of the whole-model known forward FLOPs |
macs |
macs |
MACs in the displayed row and its descendants |
train |
train |
yes / no / partial over requires_grad (auto-shown when trainability is mixed) |
mode |
mode |
train / eval from module.training as captured; mixed when the row's modules disagree |
passes |
passes |
call count |
evidence |
evidence |
weakest FLOP evidence class of the row |
mode reports the flags the capture ran under: the one-call door
defaults to execution_mode="eval", so pass execution_mode="same" (or
summarize a tl.trace capture) to see the model's own flags.
A FLOP cell whose count includes scaled_dot_product_attention ends in
^, and the footer explains it: attention FLOPs count the full
query x key square, and attention masks and is_causal are not
discounted.
import torch, torchlens as tl
from torch import nn
net = nn.Sequential(nn.Conv2d(3, 8, 3, groups=1), nn.ReLU(), nn.Conv2d(8, 8, 3, groups=8))
report = tl.summary(
net,
torch.randn(1, 3, 16, 16),
columns=["+input", "+kernel_size", "+groups", "+params_trainable", "+flops_pct", "+mode"],
)
print(report)
With NO input at all, tl.summary(model) infers a verified input via
guess_input_shape, reuses its verification trace (no second capture),
and discloses the synthesis.
The string contract¶
str(report) is canonical byte-stable ASCII with no ANSI or OSC-8 bytes
-- the equality, logging, and issue-paste contract. Unicode appears only
at explicit display boundaries:
report.print()detects the actual sink (explicit argument >TORCHLENS_SUMMARY_STYLE> CI env > verified interactive terminal > ASCII; every uncertain check falls toward ASCII).report.render("unicode")is unconditional.- The two renders differ ONLY through a declared one-to-one glyph table;
ascii == degrade(unicode)holds byte-for-byte in CI.
In Jupyter, the report (and a bare trace cell) renders a
dependency-free escaped HTML table: sticky header, right-aligned
numerics, dark-mode safe, zero JavaScript.
The result object¶
The report is a str subclass carrying detached typed data -- it retains
neither the model nor the Trace and survives cleanup and GC:
- Scalars (plain ints or None):
total_params,executed_params,unexecuted_params,trainable_params,frozen_params,total_flops_forward,total_macs_forward,unknown_flop_ops,capture_status. report.totals(SummaryTotals) also carriesparams_bytes(the unique parameters' storage),params_effective(set on a pruned model: the raw total minus mask zeros), andoptimizer_state_params/optimizer_state_bytes(set when the capture was given an optimizer).- Projections:
to_pandas(scope="display"|"all"),to_dict()(bounded, JSON-safe),to_markdown(),to_html(),render(style=),print(). report.details()serves the capture facts; the full historical provenance preamble is served byte-for-byte by the provenance group's text block,trace.provenance.summary()(it no longer opens the table).
Capture honesty travels with the table: structure-only, unverified,
partial, rescued, and episode captures banner ABOVE the table via the one
shared honesty chokepoint, and the footer's five subjects (Params /
Compute / Memory / Graph / Capture) carry coverage, both payload scopes
(at capture vs retained now), and the health verdict.
The Params footer also carries the parameter storage (size 5.42 KiB); on
a model pruned with torch.nn.utils.prune it names the pruned tensors
with raw and effective nonzero counts (pruned: 3,016 effective nonzero
of 3,124 raw (108 masked in conv.weight)); and when the capture was
given an optimizer (tl.trace(model, x,
capture=tl.options.CaptureOptions(optimizer=opt))) it adds
optimizer state N params, X KiB, read from the optimizer's live state
(zero before the first step()).
Performance¶
tl.summary runs one real capture (that is why its numbers are
measured facts, not estimates). The measured cost decomposition against a
plain forward is published and regenerated by
tools/benchmark_summary.py at docs/benchmarks/summary_performance.md.