// gat_stack_backward.mbt -- backward for the MULTI-HEAD GAT stack
// (v0.154.0).
//
// v0.153.0 gave a single GAT head a gradient. The stack around it had
// none, and the gap was not "wire the layers up" -- there were three
// structural facts the head backward knew nothing about:
//
//   1. CONCATENATION. Heads are concatenated, not averaged, so one node
//      row is `num_heads * head_width` wide and head hd owns the slice
//      `[hd*w, (hd+1)*w)`. Every upstream gradient therefore arrives as
//      one interleaved buffer and has to be UNINTERLEAVED before any of
//      the head code can run. A stack backward that sliced by the
//      concatenated width instead would hand head hd the gradients of
//      two nodes at once and pass the gate's shape checks.
//
//   2. SHARED INPUT. All heads of a layer read the SAME buffer, so
//      their input gradients SUM. This is the opposite of the head
//      backward's own accumulation (which sums over NODES into d_W);
//      getting it backwards produces a gradient scaled by the head
//      count, which is largest exactly where attention is most
//      confident.
//
//   3. THE ACTIVATION IS BETWEEN HEADS AND THE NEXT LAYER, NOT
//      AHEAD OF ONE. `graph_attention_forward` writes
//      `elu(head_output)` into the concatenated buffer, so the ELU
//      derivative multiplies the gradient of THAT buffer at the head's
//      own pre-activation -- once per hidden layer, and not at all on
//      the final layer, which is linear so its output can be read as
//      logits. `gat_layer_backward`'s `elu` flag is the single place
//      that knows this.
//
// Two of the three are "wrong by a scale factor", which is why the
// gradient gate is the only instrument that catches them: a stack
// backward with no concatenation, no head-sum, or no ELU mask still
// compiles, still runs, still produces a plausible-looking parameter
// vector -- it is simply the gradient of a different network.

// ---------------------------------------------------------------------------
// Gradients
// ---------------------------------------------------------------------------

///|
/// Parameter gradients for a whole multi-head GAT stack, indexed
/// `[layer][head]` to match `GraphAttention::layers`.
pub struct GraphAttentionGrad {
  layers : Array[Array[GATLayerGrad]]
  num_layers : Int
}

///|
/// Zero-initialised gradients for a stack.
///
/// The double seeding is deliberate and necessary, not sloppy:
/// `Array::make(n, x)` stores n REFERENCES to one value in MoonBit, so
/// seeding an outer array with one shared inner array would alias every
/// layer's head list onto layer 0's, and seeding the head list with one
/// shared `GATLayerGrad` would alias every head's `d_w` rows onto
/// head 0's. Both would pass a shape check and produce a gradient
/// multiplied by the layer and head counts. Every slot below is
/// overwritten in the loop, so the seed value itself is discarded.
pub fn GraphAttentionGrad::zero(net : GraphAttention) -> GraphAttentionGrad {
  let layers : Array[Array[GATLayerGrad]] = Array::make(
    net.num_layers, Array::make(0, GATLayerGrad::zero(net.layers[0][0])),
  )
  for l in 0..