This page documents the internal graph IR used by the PNNX converter: the C++ classes that represent the computation graph in memory, the .pnnx.param and .pnnx.bin on-disk serialization formats, Python script generation via Graph::python, and the ncnn output path via save_ncnn. For an overview of the full PNNX conversion pipeline and optimization passes, see 6.2. For the downstream ncnn .param/.bin file format consumed at runtime, see 6.1.
All IR classes live in the pnnx namespace and are declared in tools/pnnx/src/ir.h and implemented in tools/pnnx/src/ir.cpp
Class relationship overview:
Sources: tools/pnnx/src/ir.h tools/pnnx/src/ir.cpp1-330
Parameter stores a scalar or array value associated with an operator (e.g., kernel_size=(3,3), groups=1). The active field is determined by the type discriminant.
type | C++ field | Description | Serialized example |
|---|---|---|---|
0 | — | None / absent | None |
1 | b | bool | True / False |
2 | i | int | 3 |
3 | f | float | 1.000000e+00 |
4 | s | string (or %-prefixed symbolic ref) | nearest |
5 | ai | vector<int> | (1,2,3) |
6 | af | vector<float> | (1.0,2.0) |
7 | as | vector<string> | (a,b,c) |
10 | c | complex<float> | 1.0e+00+2.0e+00j |
11 | ac | vector<complex<float>> | (1.0e+00+2.0e+00j,...) |
Parameter::parse_from_string and Parameter::encode_to_string handle the round-trip. Strings containing % are treated as symbolic references (type 4) during expression building.
Sources: tools/pnnx/src/ir.h46-130 tools/pnnx/src/ir.cpp316-481
Attribute stores a named tensor (weights, biases, constants). Data is held as a raw vector<char> byte buffer with a separate shape vector and integer type code.
type | String | elemsize (bytes) |
|---|---|---|
0 | null | 0 |
1 | f32 | 4 |
2 | f64 | 8 |
3 | f16 | 2 |
4 | i32 | 4 |
5 | i64 | 8 |
6 | i16 | 2 |
7 | i8 | 1 |
8 | u8 | 1 |
9 | bool | 1 |
10 | c64 | 8 |
11 | c128 | 16 |
12 | c32 | 4 |
13 | bf16 | 2 |
Attribute::elemcount() returns the product of all shape dimensions. get_float32_data() and set_float32_data() provide f32/f64/f16 conversion helpers.
Sources: tools/pnnx/src/ir.h tools/pnnx/src/ir.cpp166-314
Operand represents a tensor value flowing between operators — the directed edge in the computation DAG.
producer: the single Operator* that writes this value.consumers: all Operator* instances that read this value.name: unique string identifier used in the .param file.type: element data type (same integer codes as Attribute).shape: dimension list. Special sentinel values:
-1: dynamic / unknown at conversion time.-233: symbolic dimension; the symbol name is stored in params["__shape__N"] where N is the dimension index.params: metadata map. Used for:
__shape__N keys: the string name of symbolic axes.__batch_index: which axis index is the batch dimension (written by solve_batch_index during the ncnn pass).Sources: tools/pnnx/src/ir.h tools/pnnx/src/ir.cpp565-627
Operator is a node in the computation graph.
type: string identifying the operation, e.g. nn.Conv2d, F.relu, torch.matmul, pnnx.Input, pnnx.Output, pnnx.Attribute, pnnx.Expression.name: unique instance name within the graph.inputs / outputs: ordered lists of Operand*.inputnames: named keys for inputs (serialized with $ prefix, used when the calling convention names arguments rather than positional indexing).params: map from string key to Parameter — operator hyperparameters.attrs: map from string key to Attribute — operator weight tensors (e.g., weight, bias).Helper methods has_param, has_attr, has_input, and named_input support pattern matching code in the pass implementations.
Sources: tools/pnnx/src/ir.h tools/pnnx/src/ir.cpp483-518
Graph owns all Operator and Operand instances via raw pointer vectors ops and operands. The destructor walks both vectors and deletes each entry.
Key methods:
| Method | Description |
|---|---|
new_operator(type, name) | Allocates an Operator, appends to ops, returns pointer |
get_operator(name) | Finds an existing Operator by name |
new_operand(name) | Allocates an Operand, appends to operands, returns pointer |
get_operand(name) | Finds an existing Operand by name |
load(parampath, binpath) | Deserializes from .pnnx.param + .pnnx.bin |
save(parampath, binpath) | Serializes to .pnnx.param + .pnnx.bin |
python(pypath, binpath, input_shapes) | Emits a runnable PyTorch Python script |
Sources: tools/pnnx/src/ir.h tools/pnnx/src/ir.cpp520-930
.pnnx.param Serialization FormatGraph::save writes a plain-text file. Graph::load reads it back. The .pnnx.bin companion file is opened in parallel as a ZIP archive via StoreZipWriter / StoreZipReader from storezip.cpp.
File structure:
7767517 ← magic number (line 1)
<operator_count> <operand_count> ← counts (line 2)
<operator line> ... ← one line per operator
Operator line grammar:
<type> <name> <in_count> <out_count>
[input_operand_names...]
[output_operand_names...]
[key=value ...] ← Parameter entries
[@key=(shape)type ...] ← Attribute entries (data in .bin)
[$key=operand_name ...] ← named input keys
[#operand_name=(shape)type ...] ← operand shape annotations
Annotation prefix summary:
| Prefix | Meaning |
|---|---|
| (no prefix) | Parameter key=value |
@ | Attribute (tensor weight); shape+type in .param, raw bytes in .bin |
$ | Named input key mapping (inputnames[i] → operand name) |
# | Operand shape annotation; ? means -1 (dynamic) |
Concrete example (from README):
7767517
4 3
pnnx.Input input 0 1 0
nn.Conv2d conv_0 1 1 0 1 bias=1 dilation=(1,1) groups=1 in_channels=12 kernel_size=(3,3) out_channels=16 padding=(0,0) stride=(1,1) @bias=(16)f32 @weight=(16,12,3,3)f32
nn.Conv2d conv_1 1 1 1 2 bias=1 dilation=(1,1) groups=1 in_channels=16 kernel_size=(2,2) out_channels=20 padding=(2,2) stride=(2,2) @bias=(20)f32 @weight=(20,16,2,2)f32
pnnx.Output output 1 0 2
Shape annotations with dynamic or symbolic dims look like:
#input=(1,?,256)f32 ← dim 1 is dynamic (-1)
#x=(1,%seq,768)f32 ← dim 1 is symbolic, named "seq"
Sources: tools/pnnx/src/ir.cpp685-930 tools/pnnx/README.md143-183
.pnnx.bin Serialization FormatThe .pnnx.bin file is a ZIP archive in store mode (no compression). Each attribute tensor is stored as a separate entry inside the archive.
Entry naming convention:
<operator_name>.<attribute_key>
For example, for nn.Conv2d operator named conv_0 with attributes weight and bias:
conv_0.weight — raw float32 bytes of shape (16, 12, 3, 3)conv_0.bias — raw float32 bytes of shape (16,)StoreZipWriter::write_file and StoreZipReader::read_file in storezip.cpp perform the reads and writes. The byte order matches the native representation of the type field directly; no additional headers are prepended.
The archive can be inspected and modified with any standard ZIP tool (e.g., 7-Zip).
Data flow between .param and .bin during load:
Sources: tools/pnnx/src/ir.cpp629-683 tools/pnnx/README.md176-186
Graph::pythonGraph::python (called in main.cpp line 407) writes a self-contained Python script that:
nn.* module operator with its saved parameters..pnnx.bin file using numpy.forward() function that calls each operator in topological order.The output file is named <model>_pnnx.py by default. It can be run directly with Python to verify inference results.
pnnx.Expression operators are expanded back to readable Python arithmetic using expand_expression() in tools/pnnx/src/ir.cpp which walks the prefix-notation expression string stored in params["expr"] and converts it to infix Python.
Sources: tools/pnnx/src/ir.cpp tools/pnnx/src/main.cpp405-408
save_ncnnAfter pass_ncnn transforms the PNNX graph into ncnn-compatible operators, save_ncnn in tools/pnnx/src/save_ncnn.cpp writes the final ncnn .param and .bin files.
The ncnn .param format is similar to the PNNX .param format in overall structure (magic number 7767517, op count, operand count, one operator per line), but differs in key ways:
| Aspect | PNNX .param | ncnn .param |
|---|---|---|
| Param keys | Named strings (bias=1, kernel_size=(3,3)) | Integer IDs (0=1, 1=3) |
| Attribute storage | ZIP archive (.bin is a zip) | Flat binary stream |
| Shape annotations | Present (#name=(...)type) | Absent |
| Named input keys | Present ($key=operand) | Absent |
In save_ncnn, only params with positive-integer string keys are written (see the string_is_positive_integer guard). Attributes are serialized as raw binary with a 4-byte magic header per tensor into the flat ncnn .bin file.
save_ncnn also generates an *_ncnn.py pyncnn inference script analogous to the PNNX Python script.
Sources: tools/pnnx/src/save_ncnn.cpp74-400 tools/pnnx/src/main.cpp418-422
GraphRewriterPassThe optimization and conversion passes (pass_level2 through pass_ncnn) transform the Graph IR using a declarative pattern-rewrite system defined in tools/pnnx/src/pass_level2.cpp and tools/pnnx/src/pass_level2.h
GraphRewriterPass interface:
| Virtual method | Purpose |
|---|---|
match_pattern_graph() | Returns a PNNXIR text string describing the subgraph to match |
replace_pattern_graph() | Returns a PNNXIR text string for the replacement (optional; if null, single-op replacement is used) |
type_str() | Type name of the new replacement operator |
name_str() | Name hint for the replacement operator (defaults to type_str()) |
match(captured_params, captured_attrs) | Optional predicate: return false to reject a structural match |
write(op, captured_params) | Copy captured parameters into the replacement operator |
PNNXIR pattern language: the same text format as .pnnx.param. Operand names prefixed with % in parameter values are capture variables. Example from torch_mm.cpp:
7767517
4 3
pnnx.Input input_0 0 1 input
pnnx.Input input_1 0 1 mat2
aten::mm op_0 2 1 input mat2 out
pnnx.Output output 1 0 out
This matches the aten::mm TorchScript op and replaces it with the single operator torch.mm.
Registration: each pass is registered at static init time with a priority integer:
REGISTER_GLOBAL_PNNX_GRAPH_REWRITER_PASS(torch_mm, 90)
Lower numbers run first. The global registry is a std::map<int, std::vector<const GraphRewriterPass*>>. pnnx_graph_rewrite in pass_level2.cpp applies one pass across all subgraph matches.
Sources: tools/pnnx/src/pass_level2.cpp1-150 tools/pnnx/src/pass_level2/torch_mm.cpp
The diagram below maps the full lifecycle of the Graph object through the conversion steps in main.cpp:
At each pass stage, the same Graph object is mutated in place. Graph::save can be called at any point to snapshot the current state for debugging (the commented-out debug.param saves in main.cpp demonstrate this).
Sources: tools/pnnx/src/main.cpp344-423
Certain Operator::type values have fixed semantics across all pass levels:
| Type | Role |
|---|---|
pnnx.Input | Graph input node; produces one output Operand per model input |
pnnx.Output | Graph output node; consumes the model outputs |
pnnx.Attribute | Constant tensor (weight/bias); stores data in attrs["data"]; converted to MemoryData by convert_attribute in the ncnn pass |
pnnx.Expression | Fused arithmetic expression; expression stored as a prefix-notation string in params["expr"]; inputs referenced as @0, @1, etc. |
nn.* | PyTorch torch.nn module operations (e.g., nn.Conv2d, nn.LayerNorm) |
F.* | PyTorch torch.nn.functional operations (e.g., F.relu, F.conv2d) |
torch.* | PyTorch top-level tensor operations (e.g., torch.matmul, torch.cat) |
Tensor.* | PyTorch Tensor member functions (e.g., Tensor.reshape, Tensor.permute) |
Sources: tools/pnnx/src/pass_ncnn/convert_attribute.cpp tools/pnnx/src/ir.cpp964-1000
Refresh this wiki
This wiki was recently refreshed. Please wait 3 days to refresh again.