Approach: use torch.fx to trace the model with a representative input, collect per-module parameter counts and compute MACs for Conv{1,2,3}D and Linear by reading input/output shapes from the trace. Sum and print a table with percentages.python
import torch
import torch.nn as nn
import torch.fx as fx
from collections import defaultdict
def estimate_flops_and_params(model: nn.Module, example_input: torch.Tensor):
model.eval()
tracer = fx.symbolic_trace(model)
# run once to populate shapes via hooks
shapes = {}
def hook_fn(module, inp, out, name):
shapes[name] = {
"in": tuple(inp[0].shape) if isinstance(inp, (list, tuple)) else tuple(torch.as_tensor(inp).shape),
"out": tuple(out.shape) if isinstance(out, torch.Tensor) else None
}
handles = []
for name, module in model.named_modules():
if isinstance(module, (nn.Conv1d, nn.Conv2d, nn.Conv3d, nn.Linear)):
handles.append(module.register_forward_hook(lambda m, i, o, n=name: hook_fn(m, i, o, n)))
with torch.no_grad():
model(example_input)
for h in handles: h.remove()
rows = []
total_params = 0
total_macs = 0
for name, module in model.named_modules():
if isinstance(module, (nn.Conv1d, nn.Conv2d, nn.Conv3d, nn.Linear)):
p = sum(p.numel() for p in module.parameters())
macs = 0
s = shapes.get(name)
if s and s["out"] is not None:
out_shape = s["out"]
if isinstance(module, nn.Linear):
# MACs = batch * out_features * in_features
batch = out_shape[0]
macs = batch * module.out_features * module.in_features
else:
# Conv: MACs ~ batch * out_channels * output_spatial * (in_channels/groups) * kernel_prod
batch = out_shape[0]
out_ch = out_shape[1]
out_spatial = 1
for d in out_shape[2:]: out_spatial *= d
kernel_elems = module.kernel_size if isinstance(module.kernel_size, tuple) else (module.kernel_size,)
kprod = 1
for k in kernel_elems: kprod *= k
macs = batch * out_ch * out_spatial * (module.in_channels // module.groups) * kprod
rows.append((name, module.__class__.__name__, p, macs))
total_params += p
total_macs += macs
# print table
print(f"{'layer':40} {'type':15} {'params':12} {'macs':15} {'%macs':8}")
for name, typ, p, m in rows:
pct = 100.0 * m / total_macs if total_macs else 0.0
print(f"{name:40} {typ:15} {p:12d} {m:15d} {pct:7.2f}%")
print(f"{'TOTAL':40} {'':15} {total_params:12d} {total_macs:15d} {100.00:7.2f}%")
Key points / assumptions:- Uses forward hooks to capture actual input/output shapes with representative input — critical for correct spatial sizes and batch effects.- Counts MACs (multiply–accumulate) for conv and linear layers only; ignores activations, BatchNorm, pooling, embedding, elementwise ops, control flow.- Assumes dense conv/linear arithmetic: ignores sparsity, quantization, Winograd or FFT optimizations, grouped/depthwise handled via in_channels/groups factor.- Uses batch-inclusive MACs (useful for throughput estimates).Limitations / misestimation sources:- Real runtime depends on memory access patterns, kernel implementation, threading, GPU/CPU specifics, fused ops, and compute-bound vs memory-bound behavior — MAC count is only a proxy.- Ignoring non-param ops (ReLU, elementwise adds, softmax) can undercount cost for models where these dominate.- Dynamic control flow or data-dependent layers not captured by single-trace input may be missed.- Quantization, sparsity, and hardware kernels change effective cost; caching, memory transfers, and I/O also affect runtime.