Skip to content

PackedLinear

MaskedLinear is the default. The RAM problem on this page is parse_adjacency() only. That layout puts every node in one state vector, so MaskedLinear(spec.to_mask()) stores an (n, n) parameter. Use kpnn2.PackedLinear on that spec's packed indices when n is large enough that the square would hurt RAM. Small recurrent graphs stay on MaskedLinear(spec.to_mask()).

parse_layered() does not build that square. Each hop mask is only that hop's (out, in), and the layer is MaskedLinear(hop.mask). PackedLinear does not take a hop mask or a LayeredSpec; there is no combined parse_layered + PackedLinear path.

Layered vs. Adjacency and the Recurrent example stay on MaskedLinear. This page is the large-n adjacency path.

The RAM problem

AdjacencySpec puts every node in one state, including a wide input layer. MaskedLinear then stores an (n_nodes, n_nodes) parameter. That square is RAM. Dataset size and minibatch size are not this problem.

Dead mask entries never affected learning. They were RAM (and extra GEMM work), not extra capacity.

What PackedLinear stores

PackedLinear stores one ordinary dense 1-D weight per live edge and updates with index_add. It is not torch.sparse and not sparse-tensor acceleration.

Use PackedLinear when n_nodes is large enough that the square would hurt. Otherwise MaskedLinear is better (GEMM, (out, in) weight).

Construction

From an AdjacencySpec:

core = kpnn2.PackedLinear(
    spec.source_index,
    spec.target_index,
    len(spec.nodes),
    len(spec.nodes),
)

Scatter inputs the same way as the Recurrent example. Input nodes have in-degree 0, so writing them into the state each step is required:

x = kpnn2.align_inputs(df, spec)
n = len(spec.nodes)
state = torch.zeros(x.shape[0], n)
state[:, spec.input_index] = x
state = torch.relu(core(state))