This document is the researcher's notebook for DEFault++. It covers the end-to-end pipeline, every command, the data flow, where each piece of code lives, how to extend the operator catalog or the diagnostic model, the Compute Canada workflow, and debugging tips.
It is not user-facing. The user-facing PyPI documentation lives in
README.md (package) and ../README.md
(repository root).
The scientific reference is the DEFault++ manuscript (in preparation).
The method is walked through in
../docs/ARCHITECTURE.md, and the frozen
runtime output schema is ../docs/SPEC.md.
DEFault++ has four moving parts. All four are in this repository.
┌─────────────────────┐ ┌──────────────────────┐
│ DEForm injector │ │ Diagnostic model │
│ (defaultplusplus. │ │ (hierarchical_ │
│ deform) │ │ graph_category_ │
│ │ │ rootcause) │
└──────────┬──────────┘ └──────────▲───────────┘
│ injects fault │ predicts
▼ │
┌─────────────────────┐ paired clean/faulty ┌─────┴────────────┐
│ Fine-tuning runs │ fine-tuning + matched │ Feature vector │
│ (HF Trainer or │ seeds, sign-flip │ (data/*.csv) │
│ manual loop) │ ─────────────────────►│ │
└──────────┬──────────┘ └──────────────────┘
│ MetricCollector
▼
┌─────────────────────┐
│ FeatureExtractor │ ← public API surface (api.py / hf_callback.py)
│ (defaultplusplus. │
│ api) │
└─────────────────────┘
Concretely:
| Stage | Code path | Output |
|---|---|---|
| Mutation injection | defaultplusplus.deform.{operators,injection,validation} |
Mutant records |
| Paired training + extraction | defaultplusplus.benchmark.runner + defaultplusplus.api.FeatureExtractor |
per-config feature vectors |
| Benchmark assembly | defaultplusplus.benchmark.dataset_writer |
data/*.csv |
| Diagnostic model training | hierarchical_graph_category_rootcause.train |
results/.../*.json |
| Ablation + plots | hierarchical_graph_category_rootcause.evaluate |
comparison table + figures |
| Post-hoc importance | hierarchical_graph_category_rootcause.posthoc_analysis |
importance JSON + plots |
| Baseline comparison | external (paper-artifact bundle, not in this repo) | per-baseline JSONs |
DEFaultplusplus-Transformer-Debugging/
defaultplusplus/ installable package + research drivers
src/defaultplusplus/ PyPI-shipped code only
__init__.py public API (FeatureExtractor, ...)
_version.py single source of truth for __version__
api.py FeatureExtractor (manual loop)
hf_callback.py DEFaultPlusCallback (HF Trainer)
config.py ExtractionConfig
extraction/
inspector.py auto-discovers HF model structure
collector.py orchestrates per-step metric modules
aggregator.py Welford running statistics
sublayer_capture.py forward hooks for FFN/LN/Q/K/V taps
feature_construction.py layer/step/epoch/phase aggregator
metrics/{attention,gradient,...}.py per-component metric modules
deform/
operators.py 52 mutation operators (the catalog)
operator_impls/ per-operator injector implementations
injection.py StaticFault, DynamicFault context managers
validation.py sign-flip permutation test, verifier
fault_config.py FaultConfiguration, Mutant types
benchmark/
cli.py defaultpp-benchmark entry point
config_grid.py enumerate (model, task, op, layer, sev)
runner.py paired runs + crash isolation
task_metrics.py per-task kill-test metric registry
dataset_writer.py shard CSV writer
diagnosis/ Predictor + load_pretrained()
processing/ FeatureProcessor + RuntimeNormalizer
pretrained/weights/ shipped encoder/decoder checkpoints
data/ benchmark download (defaultpp-bench-download)
viz/ matplotlib plots + HTML report
ui/ reserved for future CLI helpers
hierarchical_graph_category_rootcause/ research-side training (NOT in wheel)
train.py nested grouped CV training driver
evaluate.py 4-variant ablation driver
posthoc_analysis.py permutation feature/group importance
model.py HierarchicalDiagnosisModel
losses.py L_DEFault++ formula
plotting.py matplotlib figures
src/data/, src/models/ research-side helpers (NOT in wheel)
feature_groups.py 12/13 feature group taxonomy
feature_processor.py six-step preprocessing pipeline
fundamental_fpg.py FPG construction + group adjacency
loader.py CSV / pickle loaders
group_encoder.py GroupEncoder + GraphAggregator
configs/base.yaml hyperparameter config
examples/extract_*.py runnable demos
scripts/
setup.sh local venv setup
dry_run_local.sh end-to-end smoke test
build_pypi.sh clean → build → twine check → upload
cc/ Compute Canada SLURM scripts
env.sh setup_env.sh bench_array.sh merge_shards.sh train.sh ablation.sh
tests/ pytest suite
conftest.py sys.path shim for src/ + src/defaultplusplus
test_phase0_gate.py structural gate
test_phase1_gate.py feature-group gate
test_dry_run.py fast smoke (~1s)
test_feature_extractor.py real DistilBERT + GPT-2 + HF Trainer
pyproject.toml PEP 621 metadata + build config
LICENSE Apache-2.0
CHANGELOG.md version history
Makefile research-side commands
README.md package-side user reference
RESEARCH.md this file
docs/
ARCHITECTURE.md figure-by-figure method walk-through
SPEC.md output schema + architecture + scope
figures/ diagrams used across the docs
data/ DEFault-bench CSVs (fetched, not committed)
encoder_dataset.csv encoder feature traces + mutation killed labels
decoder_dataset.csv decoder feature traces + mutation killed labels
results/ generated outputs (gitignored)
The benchmark CSVs under data/ are downloaded from Zenodo with
defaultpp-bench-download (or download_bench(version="v1")) rather
than committed. The real-world GitHub-issue evaluation lives in
../realworld_evaluation/ (11 reproduced faults, one case file each).
The baseline scripts and the developer-study assets live in the
paper-artifact bundle outside this code repository.
cd defaultplusplus
bash scripts/setup.sh # creates ../.venv, pip install -e .[dev]
source ../.venv/bin/activate
make data-check # confirms data/*.csv are presentIf you need the HF Trainer test path or the example notebooks:
pip install -e ".[dev,hf]"# One-time on a login node:
bash defaultplusplus/scripts/cc/setup_env.shThis creates $SCRATCH/venvs/defaultplusplus, pre-fetches all
HuggingFace tokenizers, and points caches to scratch. Every SLURM job
script then sources defaultplusplus/scripts/cc/env.sh to load the
module stack and activate the venv. See
scripts/cc/README.md for the full pipeline.
All commands assume you are inside defaultplusplus/.
make data-check # verify ../data/*.csv exist
make train # nested grouped CV, both archs (~30 min CPU smoke)
make ablation # 4 variants × 2 archs × 5 folds
make baselines # DEFault, DeepFD, AutoTrainer, DeepDiagnosis
make all # data-check + train + ablation + baselines
make clean # rm -rf ../results/, drops __pycache__/python -m hierarchical_graph_category_rootcause.train \
--arch encoder --epochs 1 # 1-epoch smoke
python -m hierarchical_graph_category_rootcause.train \
--arch both --no-graph # ablate FPG
python -m hierarchical_graph_category_rootcause.train \
--arch both --no-sep # ablate separation loss
python -m hierarchical_graph_category_rootcause.train \
--arch both --no-graph --no-sep # basic variant
python -m hierarchical_graph_category_rootcause.evaluate \
--arch both --no-plots # all 4 variants, no figures
python -m hierarchical_graph_category_rootcause.evaluate \
--arch encoder --output ../results/runs/2026-04-29 # custom output dir
python -m hierarchical_graph_category_rootcause.posthoc_analysis \
--arch both # permutation importance
# Baseline comparison (DEFault, DeepFD, AutoTrainer, DeepDiagnosis) runs
# from the paper-artifact bundle, which is not part of this code repo.pytest tests/ # full suite
pytest tests/test_dry_run.py -v # fast smoke tests (~1s)
pytest tests/test_feature_extractor.py -v # real-model API tests (~15s)
bash scripts/dry_run_local.sh --quick # imports + smoke + FPG sanity
bash scripts/dry_run_local.sh --train # also runs 1-epoch training# 1. Bump src/defaultplusplus/_version.py and add a CHANGELOG entry.
# 2. Build:
bash scripts/build_pypi.sh # clean → build → twine check
# 3. Smoke test on TestPyPI:
bash scripts/build_pypi.sh --testpypi
# 4. Publish:
bash scripts/build_pypi.sh --pypisbatch scripts/cc/bench_array.sh # build DEFault-bench (GPU array)
sbatch scripts/cc/merge_shards.sh # concat per-task shards
sbatch scripts/cc/train.sh # train diagnostic model
sbatch scripts/cc/ablation.sh # 4-variant ablationA shared encoder feeds three classification levels:
input_features
│
▼
GroupEncoder per-group MLPs (one per of 12/13 feature groups)
│
▼
GraphAggregator H = ReLU(A_hat · H · W_msg) ×3 rounds
│
▼
projection 64-dim shared embedding z, plus group-stacked H
│
├─► detection head binary logits (faulty vs clean)
├─► category head C-class logits (11 enc / 12 dec)
└─► per-category root-cause heads + prototype matcher in H-space
Code: hierarchical_graph_category_rootcause/model.py.
L = L_detect + α·L_cat + λ_rc·L_rc + L_sep
L_sep = β·L_ctr + γ·L_pm
| Term | Code | Default |
|---|---|---|
L_detect |
detection_loss (BCE on detection head) |
weight = 1.0 |
L_cat |
category_loss (CE on category head, faulty only) |
α = 1.0 |
L_rc |
rootcause_loss (per-category CE) |
λ_rc = 1.0 |
L_ctr |
contrastive_separation_loss (SupCon over vec(H)) |
β = 0.5 |
L_pm |
prototype_matching_loss (CE over -d/τ) |
γ = 0.3 |
Code: hierarchical_graph_category_rootcause/losses.py.
Hyperparameters: configs/base.yaml.
12 encoder + 13 decoder groups defined in
src/data/feature_groups.py:
structural (FPG nodes):
attention, score, ffn_output, layernorm, residual_stream,
qkv_alignment, embedding, positional, output, cache (decoder only)
non-structural (self-loop only):
representation_drift, training_dynamics, validation_perf
Component-level FPG → group-level adjacency  (row-normalized,
self-loops). Three rounds of H = ReLU(Â · H · W_msg).
Code: src/models/group_encoder.py,
src/data/fundamental_fpg.py.
Per-group importance is computed from the predicted vs. nearest- alternative prototype margin:
Δ_g = d_g(π_alt) − d_g(π_pred)
w_g = max(Δ_g, 0) / Σ max(Δ_g', 0)
Code: HierarchicalDiagnosisModel.explain_diagnosis in model.py.
52 operators across 12 components, with three-letter IDs. They cover 45
root causes (40 for encoders, 45 for decoders; KV Cache is decoder-only).
Code: src/defaultplusplus/deform/operators.py.
Operators have four search-type categories:
| Type | Meaning | Example |
|---|---|---|
B |
binary on/off | MZM (zero attention mask) |
EU |
numeric grid | ETZ (zero N% of token embeddings) |
EL |
categorical | FCA (replace activation: ReLU/GELU/Tanh/Sigmoid) |
| Type | Class | What it does |
|---|---|---|
| Static | StaticFault |
back up parameters → mutate in place → restore on context exit |
| Dynamic | DynamicFault |
wrap module.forward with a closure → restore on context exit |
Both subclass FaultInjector, which is an AbstractContextManager.
Use them inside a with block:
from defaultplusplus.deform.injection import StaticFault
class _ZeroQ(StaticFault):
def parameters_to_mutate(self, model):
return [model.bert.encoder.layer[0].attention.self.query.weight]
def mutate_parameters(self, params):
for p in params:
p.zero_()
with _ZeroQ(model):
# forward passes here see the zero query projection
outputs = model(**batch)
# parameters restored automatically hereFive matched seeds, exact one-sided sign-flip permutation test.
Floor p-value 1/2⁵ ≈ 0.031. Code:
src/defaultplusplus/deform/validation.py.
from defaultplusplus.deform.validation import is_killed
clean = [0.90, 0.91, 0.89, 0.92, 0.90]
faulty = [0.70, 0.72, 0.68, 0.74, 0.71]
killed, p = is_killed(clean, faulty, higher_is_better=True, alpha=0.05)
# killed=True, p=0.03125 (= 1/32)The faulty class comes from killed mutants. The correct class comes from
clean base models with label-preserving perturbations. For each base model
that produces k killed mutants, generate k clean variants, test each
against the base model with the same kill test, and keep the ones that stay
indistinguishable. Code:
src/defaultplusplus/deform/clean_variants.py.
from defaultplusplus.deform import generate_clean_variants, run_one_clean_variant
variants = generate_clean_variants("bert-base-uncased", "sst2", k, base_seed=42)
for variant in variants:
sample = run_one_clean_variant(
variant, fine_tune, feature_builder,
higher_is_better=True, seeds=SEEDS, base_hyperparams=base_hp)
# sample.retained is True only when the variant is NOT killed; a
# retained sample is written with detection_label = 0.The CLI generates the correct class with defaultpp-benchmark --clean-variants N (N variants per model-task pair).
┌──────────────────┐ enumerate_configurations ┌──────────────────┐
│ BenchmarkSpec │ ─────────────────────────► │ FaultConfig grid │
└──────────────────┘ └──────────┬───────┘
│ for each
▼
┌──────────────────┐
│ run_one_config │
│ (5 paired seeds)│
└──────────┬───────┘
│ Mutant
▼
┌──────────────────┐
│ DatasetWriter │
│ (CSV shard) │
└──────────┬───────┘
│ merge
▼
┌──────────────────┐
│ data/*.csv │
└──────────────────┘
Code: src/defaultplusplus/benchmark/. The runner takes pluggable
FineTuneFn and FeatureBuilderFn callables so it can be unit-
tested without HF or GPUs (see tests/test_dry_run.py).
-
Define the operator in
src/defaultplusplus/deform/operators.py:Operator("XYZ", OperatorComponent.SCORE, "my_root_cause", "Description shown in tables.", OperatorSearchType.NUMERIC_GRID, param_name="factor", param_grid=(0.5, 2.0, 5.0), scope="both")
-
Wire the injection in
src/defaultplusplus/deform/operator_impls/registry.py: add an entry to_STATIC_SPECS(parameter mutation),_DYNAMIC_PATTERNS(forward wrap), or_ATTRIBUTE_OPS(model-attribute toggle), depending on the operator's effect.get_injector("XYZ")will then return the right injector class without further wiring; the benchmark runner resolves operators by ID through the same path. -
Add coverage in two places:
tests/test_operator_coverage.py— append"XYZ"toEXPECTED_OPERATOR_IDSso the locked operator-id list grows in step.tests/test_dry_run.py::test_all_operator_injectors_construct_verify_and_restoreiterates the full catalog, so the operator must construct on the tiny model and pass the structural verifier with no extra work.
- Add the metric to one of the existing modules in
src/defaultplusplus/extraction/metrics/(or create a new one inheritingMetricModule). - The metric's
collect()returnsdict[str, float]. Keys must follow the naming convention in the schema. - Update
../docs/SPEC.mdto add the metric and tag it asexact/approximate/reconstructed. - Add a regex token in
src/data/feature_groups.pyso the column routes to the right group.
The kill-test scoring is registry-driven so adding a task is a single-file change:
-
Register the spec in
src/defaultplusplus/benchmark/task_metrics.py:"mytask": TaskMetricSpec( name="mytask", arch="encoder", higher_is_better=True, raw_metrics=("accuracy", "f1"), # what compute_metrics must emit aggregator=lambda m: 0.5 * (m["eval_accuracy"] + m["eval_f1"]), ),
-
Wire the dataset loader in
src/defaultplusplus/benchmark/cli.py: if the task uses GLUE, add the text column tuple to_glue_text_columnsand thenum_labelsto_GLUE_NUM_LABELS. For non-GLUE encoder tasks or new decoder tasks, extend_load_encoder_dataset/_load_decoder_dataset. -
Add tests in
tests/test_task_metrics.pycovering the aggregator's arithmetic on a sampleeval_*dict and thecompute_metricscallable on synthetic logits.
The runner reads higher_is_better per configuration from the spec,
so encoder + decoder tasks can mix in a single CLI invocation.
- Add the group name to
STRUCTURAL_GROUPSorNON_STRUCTURAL_GROUPSinsrc/data/feature_groups.py. - Add a token rule to
_TOKEN_RULESso columns route into it. - If structural, add component → group mappings in
src/data/fundamental_fpg.py:_COMP_TO_GROUP. - Re-run the FPG sanity portion of the dry-run script:
bash scripts/dry_run_local.sh --quick.
- Extend
ABLATION_VARIANTSinhierarchical_graph_category_rootcause/evaluate.py. - Add a CLI flag in
train.py:mainand thread it throughrun_experiment(use_graph=, use_sep=).
defaultplusplus/src/defaultplusplus/_version.py: bump version.defaultplusplus/CHANGELOG.md: add a dated entry.bash scripts/build_pypi.sh→ check artifacts.bash scripts/build_pypi.sh --testpypi→ install in throwaway venv.bash scripts/build_pypi.sh --pypi.
HF model + batch
│
▼
ModelInspector.discover_structure() # auto-detect QKV/FFN/LN paths
│
▼
MetricCollector.collect_step(...)
├─ TrainingMetrics (loss, lr, step time, memory)
├─ GradientMetrics (per-component grad norms, update ratios)
├─ AttentionMetrics (entropy, mass_pad, head_similarity, ...)
├─ StructuralMetrics (residual cosine, ffn norms, LN stats, CKA)
├─ LogitMetrics (NLL, ECE, margin, accuracy, F1)
├─ PositionalMetrics (positional sensitivity)
└─ CacheMetrics (decoder only: cache_hidden_sim)
│
▼
EpochAggregator.update(per-step dict) # Welford running stats
│
▼ (every epoch_end)
EpochAggregator.finalize_epoch() # mean/var/burst per metric
│
▼ (at finalize)
get_final_features() # schema keys, flat
+
build_feature_vector(TrainingTrace) # layer/step/epoch/phase aggregates
│
▼
flat dict[str, float] # the feature vector
data/*.csv (DEFault-bench instances)
│
▼
prepare_dataset_from_csv (loader.py) # X, y_detect, y_category, y_rootcause
│
▼ (per outer CV fold; preprocessing fit on the fold's training data)
apply_processing_in_fold (feature_processor.py)
├─ Step 1: drop NaN > 40%
├─ Step 2: log1p high-variance cols
├─ Step 3: per-layer aggregation (encoder)
├─ Step 4: median imputation in fold
├─ Step 5: drop CV < 0.01
└─ Step 6: route columns to feature groups (Table 7.7)
│
▼
HierarchicalDiagnosisModel(group_dims, adjacency, ...)
│
▼
hierarchical_loss (5 components)
│
▼
train_one_fold (early stopping on val_metric blend)
│
▼
evaluate_one_fold (all 3 levels, oracle + predicted-category routes)
│
▼
results/.../{arch}_{variant}.json
$PROJECT and $SCRATCH are CC environment variables. Code lives in
$PROJECT; HF caches and transient artifacts live in $SCRATCH.
# One-time on the login node
bash defaultplusplus/scripts/cc/setup_env.sh
# Stage 1: build DEFault-bench (GPU array; one task per configuration)
sbatch defaultplusplus/scripts/cc/bench_array.sh
# Concatenate per-task shards
sbatch defaultplusplus/scripts/cc/merge_shards.sh
# Stage 2: train the diagnostic model
sbatch defaultplusplus/scripts/cc/train.sh
# Stage 3: 4-variant ablation study
sbatch defaultplusplus/scripts/cc/ablation.shEdit --account=def-yourgroup in each *.sh to match your CC
allocation. Edit the --array=0-N%64 range in bench_array.sh to
match the number of configurations you generated (N is the config
count minus one).
| Symptom | First check |
|---|---|
from defaultplusplus import FeatureExtractor raises ModuleNotFoundError |
run pip install -e . from inside defaultplusplus/, then re-source the venv |
make train complains about missing CSVs |
make data-check |
inspector.py fails with "Could not detect attention pattern" |
the model is from an unsupported family (e.g. T5). Pass arch= only for bert/gpt family aliases. Pull request welcome to extend inspector.py. |
cache.py raises 'DynamicCache' object is not subscriptable |
already fixed in 0.2.0; if you hit it again on a newer transformers release, extend _extract_kv_pairs |
feature_construction.py IndexError: index 1 out of bounds |
already fixed for n=1 epochs; if it returns, check _band_indices for the new shape |
| HF Trainer test skips with "accelerate is required" | pip install -e ".[hf]" |
build_pypi.sh reports "version is not dynamic" |
pyproject.toml's dynamic = ["version"] line was removed; restore it |
| sign-flip test always returns p=1.0 | argument order: is_killed(clean, faulty, higher_is_better); perplexity needs False |
# Inspect the wheel that PyPI would receive
python -c "import zipfile, sys; z=zipfile.ZipFile(sys.argv[1]); print('\n'.join(z.namelist()))" \
dist/defaultplusplus-*-py3-none-any.whl
# Inspect the FPG group-level adjacency
python -c "from src.data.fundamental_fpg import fundamental_to_feature_group_adjacency; \
names, adj, _ = fundamental_to_feature_group_adjacency('decoder'); \
print(names); print(adj)"
# Print the operator catalog
python -c "from defaultplusplus.deform import OPERATORS; \
[print(f'{op.op_id} {op.component.value:11s} {op.action}') \
for op in OPERATORS.values()]"The DEFault++ manuscript (in preparation) is the scientific reference. Section numbers below follow the thesis-chapter numbering.
| Topic | Location in the manuscript |
|---|---|
| Introduction + motivating example | Sections 7.1–7.2 |
| Fault taxonomy + operator catalog | Section 7.3, Tables 7.1, 7.2, Figure 7.3 |
| DEForm injection mechanism | Section 7.3.2, Figure 7.4 |
| Subject models / tasks | Section 7.3.3, Table 7.3 |
| Mutation validation | Section 7.3.4, Algorithm 1 |
| Benchmark statistics | Section 7.3.5, Tables 7.4, 7.5 |
| FPG construction | Section 7.4.2, Figure 7.5, Table 7.6 |
| Feature representation + Table 7.7 / 7.8 | Section 7.4.3 |
| Feature-vector construction (Equation 7.19) | Section 7.4.3.1, Figure 7.7 |
| Diagnostic model + algorithms | Section 7.4.4, Figure 7.8 |
| Equation 7.20 (group encoder) | Section 7.4.4.1 |
| Equation 7.22 (message passing) | Section 7.4.4.2 |
| Loss formulas (7.25–7.28) | Section 7.4.5, Figure 7.9 |
| Equations 7.29 – 7.30 (explanation) | Section 7.4.6 |
| Evaluation results | Section 7.5 |
| Real-world bug evaluation | Section 7.6, Table 7.20 |
| Developer study | Section 7.7 |
The scope and future extensions live in
../docs/SPEC.md §3. The runtime
extractor, the diagnostic model (diagnosis/), the single-run normalizer
(processing/), the shipped checkpoints (pretrained/), and the
benchmark download path (data/) are all complete. The remaining items
are the out-of-v1-scope architectures (encoder-decoder, sparse-attention,
Mixture-of-Experts) and distributed-training signals. The ui/
subpackage is reserved for future CLI helpers.
Apache-2.0 (LICENSE).
DEFault++ is unpublished research. If you use this code, please cite the repository directly:
@software{defaultplusplus,
title = {{DEFault++}: Hierarchical Fault Detection and Diagnosis for
Transformer Architectures},
author = {Jahan, Sigma and Rajput, Saurabhsingh and Sharma, Tushar and
Rahman, Mohammad Masudur},
year = {2026},
url = {https://github.com/SigmaJahan/DEFaultplusplus-Transformer-Debugging},
version = {0.4.1},
doi = {10.5281/zenodo.20019817},
note = {Software repository; manuscript in preparation.}
}