Validate a Ported Data Pipeline
A ported data pipeline is correct when it produces the same features as the upstream pipeline and the model predicts structures as accurately as the upstream model does. This guide collects evidence for both claims after you port a data pipeline into BioNeMo Inference Runtime (BioIR).
Validation runs at two levels:
- Level 1, feature equivalence. Compare every feature tensor that BioIR produces against the tensor the upstream pipeline produces for the same input.
- Level 2, end-to-end accuracy. Predict structures with BioIR and with the upstream model under identical settings, score both against ground truth, and compare the scores.
Level 1 finds bugs precisely: it names the sample and the feature that differ. Level 2 catches what features cannot show: postprocessing, writing, and configuration errors. Pass Level 1 before you start Level 2.
Rules for Trustworthy Results
A passing test means something only when its reference is independent and its scope is complete. Hold every validation run to these rules:
- References come from upstream code. Comparing BioIR with its own saved
output only proves that it is deterministic. Scripts that produce
references must not import
bionemo_ir.pipeline.models. - One sample set covers every step. Write the sample list once. Every step checks that it processed exactly that set: no missing, extra, or renamed samples.
- Every reference artifact has provenance. Record the command, the inputs, and the upstream version that produced each artifact.
- Tolerances stay fixed. Write them down before the first comparison. Relaxing a tolerance after a failure hides the bug that caused it. If a tolerance must change, record why.
- Nothing drops out silently. No
pytest.skip,xfail, broadexceptclauses, dropped keys, or skipped samples. An input the model cannot handle is a documented limitation, not a skipped test. - Synthetic data stays in unit tests. Hand-written tensors, mock outputs, and simplified metrics never stand in for upstream references or real scores.
- Failures leave evidence. Save the failing output and a minimal diff before you fix anything, and rerun the full sample set after the fix.
Before You Begin
Complete the following tasks before you start:
- Port and register the pipeline, as described in Port a Data Pipeline.
- Install the upstream model in its own environment, separate from BioIR, so their dependencies cannot conflict.
- Use one checkpoint for both models: the one BioIR resolves through
bionemo_ir.hubs. Convert its format for the upstream code if needed. - Use a GPU that runs both models.
Keep validation artifacts in a working directory outside the package:
Refer to the working directory and to your BioIR checkout by absolute path, so that every command and code example runs from any directory:
The Python examples use REPO and WORKDIR, defined as follows:
Fix the Sample Set
List every sample in scope in ref_data/sample_manifest.json. For each
sample, record its ID, category, input file, ground-truth file, chains with
their lengths, and multiple sequence alignment (MSA) files. Use paths
relative to REPO, as in the following example:
Use one category per sample: protein_monomer, protein_homopolymer,
protein_heterooligomer, rna, dna, ccd_ligand, smiles_ligand, or
mixed_complex. Cover every input type the model supports. A protein-only
sample set cannot validate a model that also handles ligands and nucleic
acids.
Every sample needs an experimental structure for Level 2.
examples/data/samples/gt/ has one for the bundled monomers, the 6m3u
homopolymer, the 7sfy heterooligomer, the RNA samples, and the complexes
with chemical component dictionary (CCD) ligands. A _with_template sample
uses its base sample’s file. The DNA complex (templates/7r6r.json) and the
other template samples take their names from Protein Data Bank (PDB) IDs.
Download their mmCIF files from the PDB. smiles_demo.json has no
experimental structure, so for the smiles_ligand category, write a request
that gives the ligand of a PDB complex as a simplified molecular-input
line-entry system (SMILES) string.
Run both models with the sample MSAs and templates. Without MSAs, both models score far below their real accuracy, and the comparison says little.
Every later step checks its coverage against the manifest:
Establish the Upstream Baseline
Run the upstream model on every sample and score its predictions before you test BioIR. Without the baseline, Level 2 has nothing to compare against.
Match the Inference Settings
Resolve the upstream inference configuration, print it, and save it as
ref_data/upstream_config.json. Then record in NOTES.md every setting
that changes the output, with the value that both runs use:
- Number of diffusion samples and diffusion steps
- Recycling iterations — check whether “3 recycles” means three or four passes through the trunk on each side
- Random seeds
- The rule that selects one sample from several
- Cropping or chunking of long inputs
- MSA depth, and whether each side uses MSAs at all
- Inference precision
Mismatches here produce real-looking regressions. Common mismatches include best-of-five sampling upstream against a single sample in BioIR, 200 diffusion steps against 50, and an MSA that one side never receives. When a setting cannot match, record the difference and its expected effect.
Get the Upstream Model Running
The baseline must finish on every sample. Its speed does not matter. When the upstream model crashes on an optional accelerated kernel, switch that kernel to its reference path instead of dropping the sample. Typical errors include the following:
CUDA error: no kernel image is available— a kernel built for another GPU architectureModuleNotFoundError: No module named 'flash_attn'— a missing optional package- A Triton compilation error, or an assertion that requires a newer GPU
Search the upstream configuration and command-line flags for the switch,
such as use_flash, use_deepspeed_evo_attention, or an attention-backend
setting. PyTorch’s own flash attention turns off with
torch.backends.cuda.enable_flash_sdp(False). Record every override, the
error it fixed, and its default value in NOTES.md. Reference paths compute
the same function more slowly.
Install the Scorers
Score every structure with standard tools. Do not reimplement the local distance difference test (lDDT), the template modeling score (TM-score), or DockQ. Correct implementations handle atom inclusion radii, chain mapping, symmetric homomers, residue numbering, and atom naming. This guide uses two scorers:
-
OpenStructure computes lDDT and TM-score. Install it into its own conda environment. The package is on bioconda, which resolves its dependencies from conda-forge:
If you have no conda, install Miniforge first. Call
ostby its full path. Activating the environment in a BioIR shell can break the BioIR interpreter. -
DockQ scores interfaces between chains. Install it into its own virtual environment, because it requires NumPy older than 2.0:
A scorer update can change scores. Record both versions in NOTES.md and
in the provenance of every score file, and score the upstream and BioIR
predictions with the same installs. DockQ has no version flag, so read its
version from the package metadata:
The install commands pin the tested versions, OpenStructure 2.12.0 and DockQ 2.1.3, so a rerun resolves the same scorer code. To move to newer releases, rescore the upstream baseline with them too.
Score the Upstream Predictions
Score every upstream prediction against its ground truth with the following commands:
Save the per-sample results, with the scorer versions, as
ref_data/upstream_metrics.json. Read and record the scores as follows:
- Read
lddtandtm_scorefrom the OpenStructure JSON. - Read
GlobalDockQfrom the DockQ JSON. The prediction comes first and the ground truth second. Reversed arguments score the wrong direction. - DockQ scores protein–protein, protein–nucleic-acid, and nucleic-acid
interfaces. Pass
--small_moleculefor ligand poses, and use the same flags on both sides. - A single chain has no interface: DockQ prints
Need at least two chains, exits 0, and writes no JSON. Check for the JSON file, not the exit code. Without one, recordnulland the reason, never0.0, which means “every interface is wrong.” - Suspicious scores, such as an lDDT near zero, usually mean a chain-mapping or file problem. Fix the invocation. Do not fall back to a hand-written metric.
Check coverage with assert_manifest_coverage, and compare the values with
the accuracy that the upstream authors report. A baseline far below it points
at the inputs or the inference settings, not at the model.
Check Feature Equivalence
Generate the References
The references are the features the upstream data pipeline hands to the
upstream model, one upstream_features/<sample_id>.pt per sample. Produce
them from the same upstream checkout and settings as the baseline.
Write a script that runs the upstream featurization on each manifest sample
and saves the result with torch.save, with NumPy arrays converted to
tensors. The script imports only the upstream package and standard
libraries. Make it refuse to run when any BioIR pipeline module is in
sys.modules:
When the upstream pipeline cannot run as a whole in your environment, call its featurization functions directly. They are still upstream code.
Append one provenance line per artifact to ref_data/provenance.jsonl:
The equivalence test loads a reference only when its provenance line exists
and says "source": "upstream".
Use the Upstream Production Input Format
Upstream code often accepts several formats for one kind of file, and each format goes through a different parser. The parsers can extract different fields, such as per-sequence species identifiers that only one MSA format carries, and the featurizer branches on those fields. If your reference script feeds upstream a different format than its inference entry point uses, the references differ from BioIR by construction. The difference then looks like a BioIR bug. Match the format as follows:
- Trace the upstream inference entry point from input file to features, and note which parser reads each file and which fields the featurizer uses.
- If BioIR reads format A and upstream inference reads format B, convert your inputs to format B in the reference script. Cache the converted files and record them in the provenance.
- If format B needs a field your inputs lack, fill it with a deterministic rule, such as sequential identifiers or the upstream default, and document the rule.
- Record the parser and input format in each provenance line.
Suspect a format mismatch in the following cases:
- BioIR produces extra rows whose upstream values are a parser’s
missing-value marker, such as
-1,None, or zeros. - An upstream flag is always
0, but BioIR’s is mixed. - Upstream output is empty where BioIR’s is not.
Compute BioIR Features
Run BioIR’s parser, tokenizer, and feature stages in process. The following
helper constructs every component the way build_processor does, and calls
the production stage code:
The helper runs no model, but get_pretrained_config queries the GPU, so
run the helper where a GPU is visible. Use the seed that the upstream run
used. When an upstream setting needs a different configuration value, pass a
modified copy of the configuration, and give the same object to Level 2:
Compare Every Feature
The equivalence test classifies every feature key in one table in the test file. A key is deterministic unless a documented source of randomness makes it vary, and every stochastic key carries a one-line reason. A feature that is deterministic upstream but varies in BioIR is a bug, not a stochastic feature. The following test compares every key:
Allow an extra key only when NOTES.md explains it. Print one line per key
with its category, its result, and its maximum difference or statistics, so
that a regression is visible at a glance:
Test Stochastic Features
An identical seed makes a random feature match exactly only when BioIR draws random numbers in the same order as upstream. Match the draw order where you can: it turns a stochastic key into a deterministic one. Where the order differs, test the properties that the randomness preserves:
-
Shape, dtype, and padding match exactly. The
comparefunction checks shape and dtype, and the padding mask is a deterministic key. -
Geometry matches for coordinates under a random rotation and translation per group of atoms, such as a residue’s reference conformer. The motion preserves every distance within a group. When the pipeline also samples the conformer, as Boltz-2 and OpenFold3 do, only bond lengths survive. Compare the atom pairs closer than 1.9 Å in the upstream conformer, with a tolerance that covers the conformer generator. The RDKit ETKDG conformers that OpenFold3 draws differ in bond length by up to 0.12 Å between seeds. To test the conformers themselves, match the draw order, or compare each group with the conformers that upstream can draw. The following function applies both rules:
-
Summary statistics match for sampled and masked values, such as MSA rows and masks. For a mask, the mean is the masking rate. Do not apply them to coordinates, whose mean and spread change with every rotation. The following function compares the mean and the standard deviation:
-
Distributions match across seeds, when runs are cheap. Run both sides with at least 20 seeds, and compare the per-run means with a two-sample Kolmogorov–Smirnov test (
scipy.stats.ks_2samp, p > 0.01). Skip this check when each upstream run needs GPU inference. Checks 1 to 3 catch most regressions.
compare_stochastic, which compare calls, applies check 2 to coordinate
keys and check 3 to the other stochastic keys. COORDINATES
names the group and padding-mask keys of each coordinate key, and whether
the pipeline samples a conformer per group:
Require Non-Empty References
Some features are legitimately empty for most inputs: chirality and stereochemistry constraints, distance bounds, and template features. An empty BioIR tensor then matches an empty upstream tensor even when both are wrong. For each such feature, the manifest must include a sample where the feature cannot be empty, and the test must assert that the reference is non-empty there. Such samples include the following:
- A SMILES ligand with an explicit stereocenter, such as
[C@H] - An aromatic ring
- An
EorZdouble bond - A template that aligns to the query
For templates, compare the template_* tensors on two inputs. A
self-template, the query’s own structure used as its template, exercises the
full alignment path. A multi-chain complex exercises per-chain mapping. The
no-template output must equal the upstream no-template output exactly.
Debug From the Bottom Up
Run the test on every manifest sample. When a sample fails, follow these steps:
- Save the failing output and a minimal diff under
debug/. - Find the first stage that diverges. Compare the tokenizer output, then each feature generator’s output, then each collator’s output.
- Fix the pipeline and rerun the failing sample.
- Rerun the whole manifest to confirm the fix broke nothing else.
Continue to Level 2 only when every sample passes.
Check End-to-End Accuracy
Run the BioIR Pipeline
Level 2 runs build_processor, the public entry point, with the same
samples, seeds, and inference settings as the upstream baseline. Compare the
BioIR settings with upstream_config.json before you start. The following
example runs the manifest samples serially:
If Level 1 used a modified configuration, pass the same object to
EngineProcessorConfig as engine_kwargs={"config": config}. Every stage,
including the model, then uses it.
Run the same rows through Ray as well:
Score every BioIR structure with the same OpenStructure and DockQ commands and flags as the baseline.
Compare Against the Baseline
Compare the BioIR and upstream scores for each sample in lDDT, TM-score, and DockQ:
- Pass when BioIR is lower than upstream by less than 0.05. The two implementations draw different diffusion samples, even from the same seed.
- Fail when BioIR is lower by 0.05 or more.
- Investigate before you pass when BioIR scores higher. A higher score can be real, but it can also come from different sample selection, chain mapping, scorer inputs, or settings. Pass it only after you confirm feature equivalence, identical settings, the same sample set, the shared writer, and the same scoring command.
Also require the following:
- No NaN or infinite values in any output, and plausible geometry
- An output for every sample, and no row whose
__inference_error__holds an error message - Identical per-sample scores from the serial and Ray runs
Missing samples, missing provenance, and unvalidated features fail the validation even when every score looks good.
Isolate a Failing Sample
When a sample fails Level 2, follow these steps:
-
Rerun it on both sides with three to five other seeds, and compare the score spreads. A rerun with the same seed reproduces the same structure. If the upstream and BioIR ranges overlap, sampling variance explains the gap.
-
If the ranges do not overlap, rerun Level 1 on that sample. A small feature difference can compound through the model.
-
If the features match, the problem lies after the features: in the model call, the postprocessor, or the writer. Feed the upstream features into the BioIR model once, as in Calling
forward, and save the batch and the raw output:You can then rerun the postprocessor and writer on the saved output, and score the result, without running inference again:
The writer stage uses the same mappings.
Report the Results
Attach the evidence to your pull request:
- The manifest, with its per-category sample counts
- The settings table from
NOTES.md, with every upstream–BioIR difference - Level 1 results: per sample, every key with its category, result, and maximum difference or statistics
- Level 2 results: per sample, upstream and BioIR scores for each metric, the difference, and the serial and Ray results
- Limitations: unsupported inputs with example files, and expected divergences such as fixed upstream bugs
- For every result, the command, the BioIR commit, the upstream version, the scorer versions, and the artifact paths
Related
- Port a Data Pipeline — build the pipeline this guide validates.
- Python API —
build_processor,EngineProcessorConfig, and output rows. - Benchmarks — how BioIR compares its own models with their upstream baselines.