gradian-ai/gradian: Find the data that broke your fine-tune. Contribute to gradian-ai/gradian development by creating an

github.com · 2026-08-04 · by Long Nguyen ·github
Find the data that broke your fine-tune. Contribute to gradian-ai/gradian development by creating an account on GitHub.

gradian-ai/gradian

Find the data that broke your fine-tune. Contribute to gradian-ai/gradian development by creating an account on GitHub.

3 stars · 0 forks · 1 watchers

README

Which of my training data, or which config setting, broke my fine-tune?

Documentation · Quick start · How it works · The math · Verification · Research

Gradian is a training-data attribution engine for LoRA fine-tuned LLMs. Give it a fine-tune that regressed some capability, and it tells you which training examples caused the regression, aggregated into readable clusters, cross-checked against a deterministic audit of your dataset, hyperparameters, and loss curve. It attributes model behavior through gradients, using curvature-corrected influence functions. It does not claim to invert the forward pass. That is mathematically impossible, and unnecessary. You fine-tune first, then Gradian explains. Gradian is a debugger for a fine-tune that already exists, not a trainer. Point it at a base model, the LoRA adapter you trained, the training dataset, an eval set, and optionally the run directory holding your config and trainer logs. It runs forward and backward passes to capture gradients, and never takes an optimizer step over your adapter. gradian train exists as an optional convenience for launching a LoRA fine-tune with trl, unsloth, or plain transformers, and the only part of the pipeline that needs it is counterfactual retraining, which checks that dropping the accused examples actually recovers the capability. Documentation Full documentation is at gradian.dev/docs, generated from the markdown in docs/.

Install Install paths, the supported dependency window, platform support

Quickstart Audit a run, then attribute a regression, start to finish

The workflow Where Gradian sits relative to your training

How it works The method, the math, and the design decisions behind it

A worked example One recorded run end to end: the real data files, the command, the verdict

CLI reference Every command and every flag

Configuration Every gradian.yaml field, with defaults and effects

Diagnostics All 41 checks, and what each one catches

Limitations What Gradian will not do, and the constraints that decide whether it fits your setup

Troubleshooting The errors you are most likely to hit, and the fix

Reading this as an agent rather than a person? Every page has a raw markdown twin at the same URL plus .md, the curated index is at gradian.dev/llms.txt, and the whole corpus in one file is at gradian.dev/llms-full.txt. Table of contents

Documentation Why this exists in two parts Quick start Install How it works The math Design decisions that matter Trainer agnostic by construction What is verified Limitations What is next CLI reference Python API Research resources Contributing License

Why this exists in two parts Two very different things break fine-tunes, and a tool that only does one of them is half a tool. Bad data Some cluster of your training examples taught the model something that fights the capability you care about. Finding it needs influence functions: per-example gradients over the adapter, preconditioned by an approximation of the inverse curvature, dotted against a query gradient built from the specific items that regressed. That is the engine. Bad setup Truncated completions. A loss mask that covers the prompt. No EOS, so the model never stops. Train and eval contamination. A learning rate set for full fine-tuning. Twelve epochs over 400 examples. packing=True next to completion-only masking. None of these crash, none show up as a training error, and no amount of influence math explains them better than simply checking. That is the diagnostics layer: 41 deterministic checks over your dataset, config, and trainer logs, each with evidence and a concrete fix. The report joins the two. When the cluster that hurt you turns out to be the truncated one, Gradian says so, because that changes what you should do about it. Quick start

Fastest useful thing: audit a dataset and a run. No GPU, no attribution, seconds.

gradian diagnose \ --dataset train.jsonl \ --run runs/my-finetune \ --base-model meta-llama/Llama-3.2-1B

The full thing: what data caused this capability to regress?

--adapter is the LoRA adapter you already trained. Gradian reads it, it does not produce it.

gradian attribute \ --base-model meta-llama/Llama-3.2-1B-Instruct \ --adapter runs/my-finetune/adapter \ --dataset train.jsonl \ --eval-dataset capability_eval.jsonl \ --capability json_formatting \ --out reports/ Output, in one report:

json_formatting regressed 0.910 to 0.640 (-0.270, 95% CI [-0.390, -0.150]) over 200 items. 54 items flipped from right to wrong. Cluster 7 (invoice, total, currency) accounts for 62% of the negative influence. 34 of its 40 examples have truncated completions, which points to a mechanical cause rather than a content problem. Fix max_seq_length=512 -> 1024 and retrain.

Install Gradian installs from this repository. There is no package-index release, so the installer builds the project from a branch, tag or sha. curl -fsSL https://raw.githubusercontent.com/gradian-ai/gradian/main/install.sh | sh That builds an isolated environment in ~/.gradian, picks the right PyTorch wheel for your hardware, pins the dependency window below, links gradian into ~/.local/bin, and finishes by running gradian doctor. It touches nothing else except one PATH line in your shell rc. Options cannot be flags through a pipe, because sh claims them as its own arguments. Use environment variables instead: URL=https://raw.githubusercontent.com/gradian-ai/gradian/main/install.sh

curl -fsSL $URL | GRADIAN_CPU=1 sh # force the CPU wheel curl -fsSL $URL | GRADIAN_UNSLOTH=1 sh # add the unsloth trainer curl -fsSL $URL | GRADIAN_REF=v0.1.0 sh # install a tag rather than main curl -fsSL $URL | GRADIAN_UNINSTALL=1 sh # remove it again From source git clone https://github.com/gradian-ai/gradian && cd gradian sh install.sh --local # editable install, same environment handling as above Or manage the environment yourself: pip install -e . # core: torch, transformers, peft pip install -e ".[train]" # launch fine-tunes (trl, datasets, accelerate) pip install -e ".[cluster]" # sentence-transformers and HDBSCAN (TF-IDF fallback otherwise) pip install -e ".[unsloth]" # train with unsloth (GPU only: CUDA, triton, xformers) gradian doctor # verify the dependency matrix and devices

Package Supported window

torch 2.4 to 2.11

transformers 4.51 to 5.5

peft 0.18 and above

trl 0.24 and below

That is the window Gradian is developed and tested inside. The latest upstream releases sit above parts of it, which is deliberate: a supported window that everything in the ecosystem agrees on is worth more than being current. The exact pins live in constraints/, and gradian doctor warns when your installed versions drift out of range. How it works

Three lanes run into one report. Evaluate measures what changed and refuses to attribute anything inside the noise band. Attribute turns the surviving regression into a ranked, clustered verdict over your training data. Audit checks the dataset, config, and trainer logs mechanically, with no GPU and no attribution involved. Per-example gradients in one backward pass. Attribution needs a gradient per example, but a normal backward gives the batch average. Gradian builds the batch loss as a plain sum over examples and captures each LoRA Linear layer's input and output-gradient with hooks, so one pass yields the exact per-example gradient. It is exact, it avoids vmap fragility, and it produces the factored form EK-FAC needs later for free. A batch-size-1 reference implementation and a vmap path both exist, and the test suite requires all three to agree. Engines behind one interface. DataInf is the v1 engine. graddot and tracin are the curvature-free baseline and cross-check. Because the interface is uniform, running two engines and reporting their rank agreement is a config flag, and disagreement is reported as low confidence rather than silently averaged. The math Per-example gradient, captured from hooked activations X and output-gradients G: g_j = einsum("bto,bti->boi", G, X)

Classical influence of training example j on query q, with H the Hessian at the optimum: score_j = ⟨ ∇L(z_q), H⁻¹ ∇L(z_j) ⟩

DataInf swaps the order of inversion and summation, then applies Sherman-Morrison, so no matrix is ever inverted. Per LoRA block l, with damping λ_l and n training examples: r_l = (1/λ_l)·[ g_q − (1/n)·Σᵢ (gᵢ·g_q)/(λ_l + ‖gᵢ‖²)·gᵢ ]

score_j = Σ_l ⟨ r_l, g_j⁽ˡ⁾ ⟩

The contrastive query gradient, used whenever generations are available: g_q = ∇ℓ(correct answer) − ∇ℓ(what the model actually said)

Optional Johnson-Lindenstrauss projection, which makes storage independent of model size at a measured cost in ranking fidelity: g̃ = (1/√k)·R·g R ∈ ℝ^{k×d}, R_ij ~ N(0, 1)

First-order prediction you can test by retraining: predicted_loss_delta_if_removed = score_j / n

Sign convention, fixed once. A positive score means the example reinforced the queried behavior. A negative score means it degraded it. Design decisions that matter The query gradient is contrastive by default This is the least obvious and most important design decision, and it came out of a measurement rather than a paper. For a substitution failure, where the model now says the wrong thing, cross-entropy on the correct answer gets the sign backwards. The poisoned examples come out as the most helpful data in the corpus, because what a fine-tune mostly teaches is answer format, which genuinely lowers the loss on the right answer too. Subtracting the gradient of what the model actually said cancels that shared direction.

Query gradient Planted (poisoned) examples Median percentile

CE on the correct answer +5143 (looks helpful) 93.8

Contrastive: correct minus actual output -1717 (harmful) 5.5

Invariant to normalization, module coverage, engine, and damping, across 16 configurations with the same wrong conclusion. Full measurement in docs/GPU_TESTING.md. Two resolutions Whole-example influence answers "which examples hurt me". For long documents that is often too coarse. A wrong sentence inside a 400-token entry is roughly 2 of 110 supervised tokens, so its harmful signal is diluted about 760 times relative to a short entry stating the same thing. Span-level drill-down (gradian.query.spans) keeps the tokens fixed and varies only the loss mask, scoring one gradient per sentence, and localizes the culprit sentence 92% of the time, provided the query is built from the single failing item rather than an average (33% otherwise). It says when the base model is the likelier culprit We never see the base model's training data, so its internals stay opaque. Gradian does run the base model, and that bounds the blame three ways: items the base model also fails (no capability was lost, so no training example explains them), a low base score (little was there to lose), and a thin base preference margin, meaning how strongly the base model favored the correct answer over what the tuned model now says. Below about 1 nat, the capability was held so weakly that almost any fine-tune would have tipped it, and the report says the attribution is indicative rather than proven. These are confidence qualifiers on the data verdict, stated up front in the summary, not a claim about the base model's training. Everything is content addressed The index id hashes the checkpoint, dataset, gradient spec, seed, damping, and library versions, so re-querying is free and a diagnosis is exactly reproducible. Every report prints the manifest it came from. Nothing is attributed inside the noise band A capability delta whose bootstrap confidence interval includes zero is reported and explicitly not diagnosed. Trainer agnostic by construction The core imports torch, transformers, and peft, and nothing else. It consumes a normalized RunArtifact (base model, adapter, dataset, config, logs), so a run from trl, unsloth, axolotl, or your own script all look the same. A test asserts the core never imports trl, unsloth, bitsandbytes, or xformers, even transitively. That is what keeps Gradian installable and testable on a machine without CUDA. Backends only launch training, and exist only because two things need it: the injection test and counterfactual retraining. The unsloth backend overrides exactly one method, model construction, and inherits everything else. The one unsloth subtlety worth knowing: Gradian computes its own loss from logits and never passes labels= to the model. That single decision bypasses fused cross-entropy kernels, which never materialize the gradients we capture. prepare_for_gradients() handles the rest, covering gradient checkpointing, use_cache, inference mode, and adapter requires_grad. What is verified pytest tests/ -q # 158 tests, about 4 minutes on CPU

Area What is checked

Math Sherman-Morrison against a dense linalg.solve. DataInf against a densely formed inverse Gauss-Newton (Spearman above 0.9, and required to beat the curvature-free baseline). JL projection against its additive bound.

Gradients hooks equals batch-size-1 loop equals plain autograd.grad, exactly. Padding invariance, zero gradient for unsupervised examples, bit-identical reruns.

Integration Real LoRA fine-tunes with both transformers.Trainer and trl's SFTTrainer, then index, attribute, cluster, diagnose, report.

Diagnostics Every check has a fixture that trips it and one that does not, plus a false-positive guard asserting a healthy run produces no high-severity findings.

Acceptance Plant a bad cluster in a real 135M fine-tune. The capability regresses significantly, and the planted cluster ranks first with 93% of the negative influence. Control: the same cluster answered correctly flips to +1161 (helpful) and is not blamed.

unsloth Contract level on CPU (availability without importing, compat shim post-conditions, gradient equality after prep, no labels= in the loss path). End to end is GPU gated.

GPU validation Real-hardware validation lives in docs/GPU_TESTING.md and experiments/gpu/run_gpu_suite.py, covering four experiments. The last recorded run used a 1B model on a single 16GB V100.

Experiment A pass means Last recorded run

A Data poisoning in real Alpaca data Gradian ranks the planted cluster top of negative influence Pass on the wrong-fact poison, planted cluster first with 75% of the negative influence. The refusal poison produced no gated regression to attribute, so that variant is underpowered rather than wrong

B Good data, broken hyperparameters The config checks name the real cause and do not blame the data Pass, 5 of 5 scenarios, each firing exactly the expected checks

C Counterfactual retrain Dropping the accused cluster actually recovers the capability Pass, capability fully recovered, removal precision 100%

D unsloth parity The HF and unsloth backends reach the same conclusion Not yet recorded. The last attempt predates the transformers forward snapshot and restore now in model/backends/unsloth.py, so it needs a rerun before it means anything

python experiments/gpu/run_gpu_suite.py --all --model unsloth/Llama-3.2-1B-Instruct Still open, and flagged honestly: leave-one-out correlation fidelity, an EK-FAC cross-check via kronfluence, and the projection and damping sweeps. Limitations Worth knowing before you install, because some of these decide whether Gradian fits your setup at all.

LoRA adapters only. Attribution differentiates with respect to adapter parameters, so a full fine-tune has nothing to attribute over and gradian attribute refuses without an adapter. The hook path covers LoRA on the Linear layers of a HuggingFace causal LM. LoRA on embeddings or convolutions is skipped. A 4-bit adapter is analyzed against an unquantized base. The attribution loader has no quantization path, so a QLoRA fine-tune indexes at full precision. That costs memory and moves the loss slightly relative to how the adapter was trained. One device, one process. No device_map, no DDP or FSDP, no sharding. CUDA or CPU, chosen automatically, with MPS available only if you name it. Windows needs WSL2. The gradient index is big. One vector per training example: roughly 45 MB at 1B with r=16 across all seven sites, so about 90 GB for 2000 examples, and about 340 GB at 8B. grads.module_filter cuts that about 4.3x (measured; down_proj is the largest single site, not an equal 1/7th share), grads.storage_dtype: float16 halves it, and grads.projection_dim makes it independent of model size at a measured cost in ranking fidelity (Spearman 0.6 to 0.9 against exact scores). The logit upcast, not the index, is the memory you control. It costs grads.batch_size x supervised_tokens x vocab x 4 bytes, counting only the positions the loss is computed on, and only grads.batch_size and grads.max_seq_length reduce it. It is not the whole peak: model weights and activations dominate at small scale. Measured on a 16GB V100 with a 1B model at batch 2 / 512 tokens, 80% prompt: 3.25 GB peak against 0.10 GB of upcast, rising to 4.23 GB when every position is supervised. The formula is the term that grows with your settings, so it is what to reason about when sizing a run. Measured: that same V100 runs out of memory on a 1B model at a gradient batch of 8, and runs clean at 2. Datasets are local. A .jsonl file, a .json file, or a save_to_disk HuggingFace dataset directory. Hub dataset ids are not resolved for you. Influence is a first-order approximation, not a proof. A score predicts what removing an example would do. Only the counterfactual retrain demonstrates it, which is why experiment C exists. Plenty of honest runs end in no diagnosis. If the capability delta's bootstrap interval includes zero, Gradian reports it and declines to attribute. Small eval sets land there often. The capability metric is load-bearing. A log-likelihood metric can go up even under a successful poisoning, because a fine-tune mostly teaches answer format, and it produces no generated text for the contrastive query to subtract. Use exact match or contains for factual capabilities. Alpha, and distributed from source. Version 0.1.0, installed by the script straight from this repository, or from a checkout. There is no package-index release.

What is next

An EK-FAC engine via kronfluence. The [ekfac] extra, the engine registry, and the rank agreement check are all in place already, so the engine itself is the remaining work. It matters more than the rest: a second, independently derived curvature estimate is what turns engine agreement into a real confidence signal instead of a baseline comparison. Leave-one-out correlation. Retrain N times, each omitting one cluster, and correlate the true capability change against the predicted one. It is the direct measure of whether influence tracks reality, and the single most convincing number this project can publish. Projection and damping sweeps, so grads.projection_dim and engine.lambda_scale come from a measured accuracy curve on real models rather than from a default. Recorded unsloth parity on real hardware. The compat shim and the monkey-patch restore are written and covered by CPU contract tests. Experiment D closes the loop, and has no recorded pass yet. A published package release, so installing does not need a git checkout or a repository token. Until then the installer builds from a tag or sha in this repository, which is reproducible but slower and needs access to the repository itself.

CLI reference

Command What it does

gradian doctor Environment, dependency window, devices, backends

gradian init-config Write a starter gradian.yaml

gradian diagnose Dataset, config, and dynamics audit. No GPU, no attribution

gradian checks List all 41 diagnostic checks

gradian train Optional convenience. Launch a LoRA fine-tune (hf, trl, unsloth), needed only for counterfactual retraining

grad

source

github.com · 9 images · 122 links

Comments (0)

No comments yet