BAMBOO fidelity check: xnn vs the original bytedance/bamboo, block by block#
This notebook verifies that the xnn re-implementation of BAMBOO
(xnn.hybrid.models.bamboo.BAMBOO, Gong et al. 2024, arXiv:2404.07181) is the
same function as the original bytedance/bamboo
model, block by block, by transplanting the upstream weights and comparing every
intermediate quantity.
The xnn model was written from scratch on the shared xnn abstractions
(InteratomicPotential, the xnn.transformer attention/radial primitives, the
uniform ForceStressOutput autograd forces); no upstream code is vendored. We
only import the upstream package here to compare against it.
The one intentional difference: forces. Upstream reports a
charge-non-conservative force nn_forces + coul_forces (charges held fixed)
and regularises the charge–position derivative qeq_force toward zero during
training (Supplementary Theorem A.2). xnn instead returns the full
conservative force -dE/dr uniformly via autograd, which equals upstream
forces + qeq_force to machine precision. We check exactly that identity below.
0. Setup: float64 and the upstream clone#
We run everything in double precision so the comparison is at machine precision,
and clone the upstream repository next to this notebook (skipped if already
present). Upstream imports torch_runstats.
import logging, warnings
logging.disable(logging.WARNING)
warnings.filterwarnings("ignore")
import os, sys, subprocess
import numpy as np
import torch
torch.set_default_dtype(torch.float64)
UPSTREAM = os.path.abspath("bamboo_upstream")
if not os.path.isdir(UPSTREAM):
subprocess.run(["git", "clone", "--depth", "1",
"https://github.com/bytedance/bamboo", UPSTREAM], check=True)
sys.path.insert(0, UPSTREAM)
import torch.nn as nn
from models.bamboo_get import BambooGET # the original model
print("upstream bamboo imported from", UPSTREAM)
upstream bamboo imported from /D3/sina/xnn/examples/fidelity_checks/bamboo_upstream
1. Build both models with identical hyper-parameters#
A small-but-real BAMBOO: feature width dim=64, num_rbf=32, 3 GET layers,
16 attention heads, cutoff 5 Å (the upstream defaults).
import xnn
from xnn.common.config import from_dict
from xnn.common.data import structure_to_graph
from xnn.common.models import ForceStressOutput, build_model
DIM, NRBF, NLAYERS, NHEADS, CUT = 64, 32, 3, 16, 5.0
up = BambooGET(
device=torch.device("cpu"),
coul_disp_params={"coul_damping_beta": 18.7, "coul_damping_r0": 2.2, "disp_cutoff": 10.0},
nn_params={"dim": DIM, "num_rbf": NRBF, "rcut": CUT, "charge_ub": 2.0,
"act_fn": nn.SiLU(), "charge_mlp_layers": 2, "energy_mlp_layers": 2},
gnn_params={"n_layers": NLAYERS, "num_heads": NHEADS, "act_fn": nn.GELU()},
).eval()
cfg = from_dict({"model": {"name": "bamboo", "cutoff": CUT, "n_features": DIM,
"n_rbf": NRBF, "n_interactions": NLAYERS, "extra": {"num_heads": NHEADS}}})
xm = build_model(cfg.model).eval()
print("xnn params :", sum(p.numel() for p in xm.parameters()))
print("upstream params:", sum(p.numel() for p in up.parameters()))
xnn params : 103620
upstream params: 306750
2. The transplant map (upstream → xnn)#
Every upstream tensor maps to exactly one xnn tensor. Because the read-out MLPs
and the transformer projections keep matching shapes, most transplants are a
plain load_state_dict.
upstream |
xnn |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
… |
|
def copy(dst, src):
dst.load_state_dict(src.state_dict())
copy(xm.atom_emb, up.atom_embtab)
copy(xm.dis_rbf, up.dis_rbf)
copy(xm.rbf_proj, up.rbf_proj)
copy(xm.energy_mlp, up.energy_mlp)
copy(xm.charge_mlp, up.charge_mlp)
copy(xm.electronegativity_mlp, up.pred_electronegativity_mlp)
copy(xm.hardness_mlp, up.pred_electronegativity_hardness_mlp)
up_layers = [up.first_attn] + list(up.attns) + [up.last_attn]
for xl, ul in zip(xm.layers, up_layers):
copy(xl.attn.qkv_proj, ul.qkv_proj)
copy(xl.attn.layer_norm, ul.layer_norm)
copy(xl.output_proj, ul.output_proj)
if xl.vec_proj is not None:
copy(xl.vec_proj, ul.vec_proj)
print("weights transplanted")
weights transplanted
4. Block 1: atom embedding, radial basis, cutoff#
The very first tensors: the type embedding x^0, the exponential-normal radial
basis dis_rbf(r), and the cosine cutoff envelope.
def maxabs(a, b): return float((a - b).abs().max())
# reproduce the upstream front-end tensors
coord_diff = up_inputs["edge_cell_shift"]
r = coord_diff.norm(dim=-1)
unit = coord_diff / r.unsqueeze(-1)
up_x0 = up.atom_embtab(up_inputs["atom_types"])
xm_x0 = xm.atom_emb(g.atomic_numbers)
print("atom embedding :", maxabs(up_x0, xm_x0))
print("radial basis :", maxabs(up.dis_rbf(r), xm.dis_rbf(r)))
print("cutoff envelope :", maxabs(up.cutoff(r), xm.cutoff_fn(r)))
atom embedding : 0.0
radial basis : 0.0
cutoff envelope : 0.0
5. Block 2: the edge feature and the equivariant edge vector#
edge_feat = act(W_rbf · rbf(r)) reshaped per head, and the equivariant edge
vector e_ij = edge_feat ⊗ r_hat_ij (built from the unit edge direction, as
in both codes’ front-ends). We reuse this single edge_feat/edge_vec pair to
drive the GET layers of both models below.
H, DPH = NHEADS, DIM // NHEADS
weights_rbf = up.dis_rbf(r)
ef = up.rbf_proj(weights_rbf).reshape(-1, H, DPH)
ef_x = xm.rbf_proj(weights_rbf).reshape(-1, H, DPH)
print("edge feature :", maxabs(ef, ef_x))
edge_feat = ef # identical, reuse for both
edge_vec = edge_feat.unsqueeze(-3) * unit.unsqueeze(-1).unsqueeze(-1)
print("edge vector shape:", tuple(edge_vec.shape))
edge feature : 0.0
edge vector shape: (42, 3, 16, 4)
6. Block 3: every GET layer, one at a time#
Each Graph Equivariant Transformer layer is driven with identical inputs and its scalar (and, except in the last layer, vector) update is compared. This is the heart of the model: multi-head QKV attention on edges, coupled to the scalar and vector channels.
row, col = up_inputs["edge_index"][0], up_inputs["edge_index"][1] # centre, neighbour
radial = up.cutoff(r)
x0 = up_x0 # identical embedding (Block 1)
# --- first layer (no vector input) ---
u_df, u_dv = up.first_attn(x0, edge_feat, edge_vec, row, col, radial, N)
x_df, x_dv = xm.layers[0](x0, edge_feat, edge_vec, None, row, col, radial, N)
print(f"layer 0 (first): d_scalar={maxabs(u_df, x_df):.2e} d_vector={maxabs(u_dv, x_dv):.2e}")
node_feat_u, node_vec_u = x0 + u_df, u_dv
node_feat_x, node_vec_x = x0 + x_df, x_dv
# --- middle layers ---
for i, (ul, xl) in enumerate(zip(up.attns, xm.layers[1:-1]), start=1):
u_df, u_dv = ul(node_feat_u, edge_feat, node_vec_u, edge_vec, row, col, radial, N)
x_df, x_dv = xl(node_feat_x, edge_feat, edge_vec, node_vec_x, row, col, radial, N)
print(f"layer {i} (mid) : d_scalar={maxabs(u_df, x_df):.2e} d_vector={maxabs(u_dv, x_dv):.2e}")
node_feat_u = node_feat_u + u_df; node_vec_u = node_vec_u + u_dv
node_feat_x = node_feat_x + x_df; node_vec_x = node_vec_x + x_dv
# --- last layer (no vector output) ---
u_df = up.last_attn(node_feat_u, edge_feat, node_vec_u, row, col, radial, N)
x_df, _ = xm.layers[-1](node_feat_x, edge_feat, edge_vec, node_vec_x, row, col, radial, N)
print(f"layer {NLAYERS-1} (last): d_scalar={maxabs(u_df, x_df):.2e}")
print("final node features:", maxabs(node_feat_u + u_df, node_feat_x + x_df))
layer 0 (first): d_scalar=0.00e+00 d_vector=0.00e+00
layer 1 (mid) : d_scalar=0.00e+00 d_vector=0.00e+00
layer 2 (last): d_scalar=0.00e+00
final node features: 0.0
7. Block 4: charges, electronegativity/hardness, and the energy pieces#
The full upstream energy_nn gives the NN energy, the (conserved) partial
charges and the electronegativity energy; we compare against the xnn forward
outputs. Note xnn spreads the electrostatic energy over node_energy so that
energy == sum(node_energy), so we compare the aggregated component energies.
nn_energy_u, charge_u, eneg_u = up.energy_nn({
**up_inputs, "edge_cell_shift": up_inputs["edge_cell_shift"].clone()})
xout = xm(g)
# xnn aggregates electrostatics into node_energy, so compare component sums:
# upstream (coulomb + electronegativity) == xnn energy_elec.
print("partial charges :", maxabs(charge_u, xout["charges"]))
print("NN energy :", abs(float(nn_energy_u[0]) - float(xout["energy_nn"][0])))
up_predict = up.predict({**up_inputs, "edge_cell_shift": up_inputs["edge_cell_shift"].clone()})
elec_up = float(up_predict["energy"][0]) - float(nn_energy_u[0]) # coul + electronegativity
print("electrostatic energy:", abs(elec_up - float(xout["energy_elec"][0])))
partial charges : 3.608224830031759e-16
NN energy : 0.0
electrostatic energy: 1.7763568394002505e-14
8. Block 5: the full model (energy, forces, charges, dipole)#
Finally the whole model through the uniform ForceStressOutput. Energy,
charges and dipole match directly; the xnn conservative force equals the
upstream forces + qeq_force (see the note at the top).
fs = ForceStressOutput(xm, compute_forces=True)
xfull = fs(g)
up_out = up.predict({**up_inputs, "edge_cell_shift": up_inputs["edge_cell_shift"].clone()})
e_rel = abs(float(xfull["energy"][0]) - float(up_out["energy"][0])) / abs(float(up_out["energy"][0]))
full_force = up_out["forces"] + up_out["qeq_force"]
print(f"total energy (rel) : {e_rel:.2e}")
print(f"partial charges : {maxabs(up_out['charge'], xfull['charges']):.2e}")
print(f"dipole : {maxabs(up_out['dipole'][0], xfull['dipole'][0]):.2e}")
print(f"conservative force : {maxabs(full_force, xfull['forces']):.2e}")
print(f" (vs upstream nn+coul only: {maxabs(up_out['forces'], xfull['forces']):.2e} — the qeq residual)")
total energy (rel) : 6.53e-16
partial charges : 3.61e-16
dipole : 3.55e-15
conservative force : 1.07e-14
(vs upstream nn+coul only: 4.69e+00 — the qeq residual)
Summary: every block matches#
block |
quantity |
agreement |
|---|---|---|
1 |
atom embedding, radial basis, cutoff |
machine precision |
2 |
edge feature, equivariant edge vector |
machine precision |
3 |
every GET layer’s scalar & vector update |
machine precision |
4 |
partial charges, component energies |
machine precision |
5 |
total energy, charges, dipole |
machine precision |
5 |
conservative force |
|
The xnn BAMBOO is the same function as bytedance/bamboo, block by block.
The only difference is deliberate and documented: xnn returns the full
conservative -dE/dr (equal to upstream forces + qeq_force), whereas upstream
splits off the charge-equilibrium residual qeq_force as a training penalty.