Download docs/DETAILS.md from Upwork-AIML/Conditional_Causal_Difficulty_for_LLMs: direct link, hf CLI and curl.
- Browser
- Download file 41.3 kB
-
https://huggingface.co/Upwork-AIML/Conditional_Causal_Difficulty_for_LLMs/resolve/main/docs/DETAILS.md
- Command line
-
hf download hf://Upwork-AIML/Conditional_Causal_Difficulty_for_LLMs/docs/DETAILS.md
-
curl -L -o DETAILS.md https://huggingface.co/Upwork-AIML/Conditional_Causal_Difficulty_for_LLMs/resolve/main/docs/DETAILS.md
Conditional Causal Difficulty for LLMs: full documentation
Short version: see the README. This is the long reference.
Code for "Fragility Is Not Reducible to Hardness: Conditional Causal Difficulty for LLMs" (arXiv:2611.02065).
LLMs show large, seemingly arbitrary performance drops when a task is rewritten in ways that leave its requirements untouched. We treat task difficulty as a causal quantity β the treatment effect of meaning-preserving perturbations on a model β and ask which tasks are anomalously fragile, beyond what their complexity predicts.
This repository contains everything needed to reproduce the paper:
- the estimands CCD (conditional causal difficulty) and CRD (complexity-residualized difficulty), with cross-fitted residualization;
- the predictors Frail-Ο (CCD from text) and Frail-Ο (the unconditional contrast, used for Synth-CD and as a control);
- the GlassGSM and GlassMATH benchmarks: construction pipelines, the No-Op contamination audit, and the multi-operator-mix corpora;
- the Synth-CD mechanism-check suite and every baseline (BERTPE, VAP3, PromptSET measures, surface features, IRT, β¦);
- the reliability-ceiling, identity-twin-floor, curation, and solver-transfer analyses.
Naming. The benchmarks are called GlassGSM (twins of GSM8K) and GlassMATH (twins of Hendrycks MATH). Older config and file names still use
gsmic_adv(= GlassGSM) andmath_adv(= GlassMATH). They are the same thing.
Headline results (from the paper)
Test-fold Spearman, normalized by each suite's reliability ceiling (0.969 GlassGSM, 0.972 GlassMATH), mean Β± sd over five seeds (Table 2 of the paper):
Method (predicting CCD, Ο) |
GlassGSM | GlassMATH |
|---|---|---|
| Frail-Ο (fine-tuned encoder) | 0.613 Β± 0.023 (raw Ο_S 0.594) | 0.557 Β± 0.023 (raw Ο_S 0.541) |
| VAP3 | 0.444 Β± 0.049 | 0.463 Β± 0.010 |
| BERTPE | 0.420 Β± 0.051 | 0.451 Β± 0.015 |
| Surface features (GBT) | 0.171 Β± 0.023 | 0.401 Β± 0.014 |
| Declared complexity only | 0.207 Β± 0.060 | 0.285 Β± 0.026 |
Other headline findings:
- Fragility is a different quantity from hardness. GlassGSM raises fragility from
3.7% (GSM-IC-style distractors) to 44.9% on the same problems while keeping the
answer key unchanged. The measured base pass rate
Θ²(J)is negatively correlated with CCD. - CRD is orthogonal to the declared complexity proxy (|Corr(target, C)| β€ 0.06 on the m=9 run), and the contrast architecture is complexity-orthogonal even without explicit residualization on Synth-CD (Frail-Ο Ο_S = 0.617 at |Ο_C| = 0.028).
- Actionable for curation. On GlassGSM, selecting the top 5% of problems by Frail-Ο gives Recall@5% = 0.099 of truly fragile items vs 0.05 for random and 0.100 for an oracle ranking.
- Partial, solver-dependent transfer. Training Frail-Ο on one solver's CCD and
testing on another's transfers only partially, as expected because the stratum is
defined by each solver's own
Y(J). - Measurement floors. The identity-twin floor (break rate of re-solved, unperturbed bases) is 1.1% on GlassGSM and 2.5% on GlassMATH.
See Β§11 for what to expect when re-running.
Table of contents
- The estimands: CCD and CRD
- Models: Frail-Ο and Frail-Ο
- Datasets
- Repository layout
- Installation
- Quickstart (no API access needed)
- Reproduction guide
- Orchestration scripts
- Configuration reference
- Outputs and tests
- Reproducibility notes
- Citation, license, third-party material
1. The estimands: CCD and CRD
A task (prompt) is a token sequence J. An LLM maps it to a response O(J), which
a quality-scoring function phi collapses to a scalar
Y(J) = phi(O(J), J) in [0,1]
(For the math suites this is exact match on the extracted modal answer over 5 samples
at temperature 0.2.) A meaning-preserving perturbation operator (MPPO) T
rewrites a task into a twin T(J) with the same requirements and the same gold
answer but a different surface form. Let P be a distribution over such operators.
Per-instance contrast. Each task has a control score Y(J) and a treatment score
Y(T(J)); the per-instance effect is
Delta(J, T) = Y(J) - Y(T(J)), tau(J) = E_{T ~ P}[ Delta(J, T) | J ]
T is drawn independently of every attribute of J, and Delta compares the same
task under two randomly assigned surface realizations β a randomized experiment
within the item. Its contrast is unbiased without adjustment; the complexity
back-door S <- C -> Y only opens for observational comparisons across prompts,
which is what every Y(J)-predictor baseline estimates.
Why the unconditional average (U-CCD) is the wrong target
A meaning-preserving rewrite can only be observed to hurt a task the model already
solves. On the stratum Y(J) = 0, every realized contrast satisfies
Delta(J, T) <= 0: a task the model cannot solve at all is recorded as maximally
robust rather than untested β the stratum is censored, not informative. The
unconditional average D_avg(J) = E_T[Delta(J, T)] therefore mixes a live effect with
a structural zero, and empirically it is unlearnable on the real suites: against U-CCD
on GlassGSM every method, ours included, is weak (best Ο_S = 0.162, surface length,
vs Frail-Ο's 0.594 on CCD). U-CCD results are retained as a control
(paper_supports_real.py --estimand d_avg).
CCD β conditional causal difficulty
Restrict the estimand to the solvable stratum (Y(J) = 1) using the per-twin
break event
B(J, T) = 1{ Y(J) = 1 and Y(T(J)) = 0 }
On this stratum Delta(J, T) = B(J, T) exactly, so CCD is the treatment effect
evaluated where it is non-degenerate:
pi(J) = E_{T ~ P}[ B(J, T) | Y(J) = 1 ]
pi(J) β the conditional causal difficulty β is the fraction of the twin family
that breaks a task the solver solved. It is a principal-stratum quantity
(stratification on the control potential outcome). The paired design makes it the
tractable case: stratum membership is observed (the base and every twin are actually
run), and conditioning on Y(J) opens no selection path because T is independent
of J.
Dose response. A twin's dose m in {1, 3, 5, 7, 9} indexes the perturbation
budget of a corpus file (see Β§3). Fragility is summarized by a logistic
dose response
logit Pr[ B(J, T) = 1 | m ] = a_J + b_J (m - m_bar)
where a_J is fragility at typical dose and b_J the degradation rate. Frail-Ο
learns the per-perturbation break probability and aggregates it analytically to
pi and to (a_J, b_J), instead of regressing a job-level scalar.
CRD β complexity-residualized difficulty
Harder tasks tend to be more phrasing-sensitive, so intrinsic complexity C is an
effect modifier, not a confounder: the raw pi of a task is complexity-inflated.
To isolate anomalous fragility β the part the declared complexity cannot account
for β we residualize C out. Fix a base quantity Q in {tau, pi}, a declared
complexity observable C, and a task population p (restricted to Y(J) = 1 when
Q = pi). With m_{Q,C,p}(c) = E[ Q(J') | C(J') = c ]:
D^perp_{Q,C,p}(J) = Q(J) - m_{Q,C,p}( C(J) )
Properties (Proposition 1 of the paper):
D^perpis theL2projection residual ofQonto measurable functions of the declaredC; henceE[D^perp] = 0andD^perpis uncorrelated with every function ofC.- It is estimated by cross-fitted residualization: regress the measured
Q_haton the complexity proxy out-of-fold (folds shared with the main evaluation) to getm_hat, and setD^perp_hat = Q_hat - m_hat. This removes own-observation leakage and is consistent wheneverQ_hat -> Qand the complexity regression is consistent. D^perp = 0for a task exactly as fragile as the typical same-Ctask;D^perp > 0for anomalously fragile ones. Orthogonality is to the declared proxy only β a differentCwould give a different CRD.
The declared proxies are suite-specific and external to the pipeline: the
problem-setter-assigned competition level for GlassMATH, and a complexity score
computed from the problem text for GlassGSM (see Β§11).
Implementation: src/causal_difficulty/eval/deconfound.py (cross_fitted_residual,
orthogonality_diagnostics, stratified_rank_correlation), exercised by
scripts/fragility_report.py (orthogonality block), scripts/paper_supports_real.py,
and scripts/solver_crd_vs_ccd_transfer.py.
2. Models: Frail-Ο and Frail-Ο
The repository contains two model families. They share losses and utilities but are trained by different entry points, so it matters which one a script uses.
Frail-Ο β break-probability model (the headline predictor for CCD)
Implemented in models/fragility.py (frozen features + MLP) and
models/fragility_encoder.py (joint encoder fine-tuning / LoRA), driven by
scripts/fragility_report.py.
It predicts the probability that a single perturbation at a given dose breaks a given task,
p(job, m) = sigmoid( f([ x_job, dose_features(m) ]) )
and is trained with binary cross-entropy on every (job, twin) observation rather
than on a job-level scalar. Job-level estimands are then computed from p in closed
form: pi is p averaged over the dose composition of the evaluation design,
(a, b) come from a least-squares fit of logit p(job, m) over doses, and
worst-case / quantile targets at budget K follow from 1 - (1 - p)^K and the
binomial tail. One model therefore serves every estimand at every budget.
- Paper configuration:
--featurizer hf_finetunefine-tunes a pretrained encoder (DeBERTa-v3-base by default) end to end, with encoder LR2e-5and head LR1e-3. Frozen encoders and LoRA are the ablations in the paper. - Contrast supervision is optional.
--lambda-contrast(default0.0) adds a within-job contrast term between two twins' predicted break probabilities. At the default, training is the plain per-observation BCE objective. - Other objectives:
--loss-mode {bce, soft_rank, reliability_weighted}. - Two protocols, never merged:
text(problem statement only) andtext_probe(adds statistics of repeated solver samples of the base prompt β requires model access; see Step 4). - The CRD prediction subtracts the cross-fitted complexity regression from the
prediction,
D^perp_hat(J) = D_hat(J) - m_hat_pred(C(J)).
Frail-Ο / SCE β Siamese Counterfactual Encoder (unconditional contrast)
Implemented in models/sce.py, models/train.py, models/losses.py
(scripts/train.py, scripts/evaluate.py, scripts/paper_supports_*.py). A shared
encoder embeds the base J and the twin T(J) into h and h'; their difference
feeds a contrast head, while a single-tower ex-ante head predicts difficulty
when no twin exists:
Delta_hat(J, T) = psi_rho( h - h' ) # contrast head (per-observation supervision)
D_hat(J) = g_omega( h ) # ex-ante head (deployable, base text only)
L = sum_T ell_delta( Delta_hat(J,T), Delta(J,T) )
+ ell_delta( g_omega(h), D^obs(J) )
+ lambda * R_inv(theta; J)
R_inv = E[ || Pi_C h - Pi_C h' ||^2 ] is an invariance regularizer with a learned
complexity-subspace projection Pi_C, encouraging h = c(C(J)) + b(R(J), S(J)) so the
complexity term cancels in h - h' and the contrast depends on surface form alone.
Training is a two-stage curriculum: a label-free representation stage (triplet loss
pulling twins together and pushing requirement-changing edits apart), then a small
score-labeled calibration stage.
Frail-Ο is used on Synth-CD, where scores are continuous, no stratum is censored,
and tau is well posed, and for the U-CCD control on real suites. Because T leaves
C and the requirements fixed, the contrast h - h' exposes only the surface part of
the embedding β which is why the contrast head is intrinsically complexity-orthogonal
(verified on Synth-CD without any explicit residualization).
Featurizers and baselines
Featurizers (--featurizer): tfidf_svd (default, frozen, CPU), surface,
hashing, openai_embed (text-embedding-3-small), hf_frozen / minilm / bge /
hf:<model-id> (frozen sentence encoders), and the trainable hf_finetune
(Frail-Ο) and hf_lora.
Baselines (src/causal_difficulty/baselines/): surface features (GBT), declared
complexity score, frozen twin-distance, Prompt Clarity / Embedding Entropy /
Embedding Specificity-IEF (PromptSET), BERTPE, VAP3 (retrained to regress the break
event), measured base correctness Y(J), amortized IRT, score predictor, embedding
linear probe, text length.
3. Datasets
3.1 GlassGSM and GlassMATH
Both suites augment an original math benchmark with twins from three meaning-preserving operator families:
| Family | Operator tag | What it does | How it is built |
|---|---|---|---|
| Entity renaming | entity_rename |
Replaces recognised person names with names from a neutral pool | deterministic (data/math_surface_ops.py) |
| Sentence reordering | sentence_reorder |
Reorders setup sentences; the question stays last | deterministic (data/math_surface_ops.py) |
| Distractor injection | math_irrelevant_context_m{m} |
Splices in setup sentences (no question) from a different GSM8K problem | scripts/gsmic_adv_pipeline.py, math_adv_pipeline.py |
GlassGSM (gsmic_adv) |
GlassMATH (math_adv) |
|
|---|---|---|
| Base problems | 1,319 (GSM8K test) | 4,999 (Hendrycks MATH test) |
| Pairs in the five dose files | 32,858 | 122,847 |
| Unique twins (dedup on twin text) | 18,309 (13.9 / problem) | 115,364 (23.1 / problem) |
Declared complexity proxy C |
heuristic score from the problem text | MATH difficulty level 1β5, mapped to complexity_score β [0.05, 0.95] |
| Fragility vs original GSM-IC-style | 3.7% β 44.9% | β |
| Public release | reza2ei/GlassGSM on the Hub (download to release/glassgsm/) |
reza2ei/GlassMATH on the Hub (download to release/glassmath/) |
Solvers. Reference solver gpt-4o-mini; cross-solver and cross-generator runs use
Qwen3.7-plus and DeepSeek-v4-pro (and Llama-3.1-8B in some scripts). Solvers decode at
temperature 0.2 with 5 samples per prompt; Y scores the modal extracted answer,
so a twin can "break" a task by re-sampling alone β that floor is measured with
identity twins (Step 8).
3.2 The multi-operator-mix corpus (the paper's benchmark)
Per-dose pipeline runs produce one file per dose. The paper's benchmark instead uses
multi-operator-mix files: for each dose m, a file holds about m twins per base
problem, split as evenly as possible across the families present for that problem.
Each twin applies one operator. scripts/build_multiop_mix.py builds them without
any new LLM calls by resampling audited multi-operator data:
--mix-twins mis the total number of twins per base problem, shared uniformly acrosssentence_reorder,entity_rename,math_irrelevant_context; the sampler is seeded and reproducible;math_irrelevant_contexthas a per-twin injection size (number of stacked sentences, baked into sibling files named..._m{size}_multi_...), and the resampler picks the size whose file most closely matches that family's share ofm;d_obs/d_wcare recomputed over exactly the sampled twins.
The five dose files of a suite are resamples of the same audited pool, not
independent draws: treat them as the same problems seen at different perturbation
budgets, and always split by base_id, never by row or by file.
data_audited_multiop_mix/{suite}_m{m}_multiop_mix_{gen}_labeled_pairs.jsonl
suite in {gsmic_adv, math_adv}
m in {1, 3, 5, 7, 9}
gen in {4omini, qwen3p7plus, deepseekv4pro, llama} # solver / generator tag
- the pooled 5-dose run (the five files concatenated) is the source of the
headline CCD table (
pi+ dose-responsea/b); - the
m = 9slice alone is the source of the CRD orthogonality table and of the cross-model and solver-transfer protocols.
3.3 No-Op contamination audit
The No-Op requirement (a twin must not change the correct answer) must hold
empirically, not just by construction. scripts/audit_contradictions_and_filter.py
adjudicates every (base, twin) pair and writes a filtered copy of the benchmark
(raw files are never modified). A pair is dropped if any of the following holds:
- LLM No-Op adjudication β an LLM adjudicator receives the base text, twin text,
and gold answer and judges whether the perturbation changes or invalidates the
correct answer (default
gpt-4o-mini; the paper's pipeline used a stronger model with a second adjudicator). A "contradiction" verdict, or an unparsable response after retries, drops the pair. - Entity-name collision β the injected distractor reuses a proper noun from the target problem (calendar words excluded).
- Gold-answer-number collision β the injected text contains the numeric value of the correct answer.
- Multi-question collision β the injected distractor contains its own
?. - Unit-compatible quantity collision β flagged but not dropped: a second adjudicator pass over the full flagged pool found no actual contradiction.
Structural checks are deterministic (src/causal_difficulty/features/collision.py);
the shared verdict-parsing library is scripts/contradiction_audit_lib.py. Audit reuse
across doses and scoring models: audit_contradictions_dose_reuse.py,
audit_crossmodel_reuse.py. Sample-level rate estimation:
scripts/noop_verification.py, scripts/human_answer_preservation_audit.py.
3.4 Labeled-pair data format
Everything downstream consumes one JSONL of LabeledPair objects
(src/causal_difficulty/schemas.py):
{
"base_id": "gsmic_adv-42",
"twin_id": "gsmic_adv-42-d7",
"operator": "math_irrelevant_context_m9",
"category": "math",
"complexity": "medium",
"complexity_score": 0.72,
"base_text": "...",
"twin_text": "...",
"delta_per_model": {"gpt-4o-mini": 1.0},
"delta_mean": 1.0,
"d_obs": 0.35,
"d_wc": 1.0,
"y_base_mean": 1.0
}
operatorcarries the family and the dose (_m{k}suffix);eval/loading.pystamps the dose from the filename onto legacym=1rows that lack the suffix (otherwise the lowest dose would be read as dose 0 and flatten the dose-response fit);delta_mean = Y(J) - Y(T(J))for the scoring model; the per-observation break eventB = 1{Y(J)=1, Y(T(J))=0}is derived fromy_base_meanand the per-model deltas byeval/estimands.py(build_job_targets), which also computes per-jobpi, dose-response(a, b), and the solvable-stratum restriction;d_obs/d_wcare job-levelD_avg/ worst-case targets (Frail-Ο runs).
3.5 Synth-CD (synthetic mechanism checks)
scripts/synth.py / synth2.py generate synthetic jobs with known causal ground
truth: two independent confounders C ~ Beta(2,2) (structural complexity) and
E ~ Beta(2,2) (domain expertise), 8 domains, 12 operator types spanning
cosmetic-only through dual-sensitive, with text length controlled to be independent of
C and E. simulate.py / simulate2.py label twins with
delta(J, op) = max(0, mu_op (1 + C(J)) + zeta_op E(J) + eps)
calibrated so Spearman(D_avg, C) β Spearman(D_avg, E) β 0.60. Scores are continuous
and no stratum is censored, so U-CCD is well posed here; it is used as a mechanism
check (does the contrast head recover the expertise signal with near-zero |rho_C|
without explicit residualization?), not as a headline setting.
scripts/synth_frailpi.py + synth_frailpi_baselines.py provide the Bernoulli-break,
dose-tiered variant used for the Synth-CD consistency table.
4. Repository layout
src/causal_difficulty/
models/
fragility.py Frail-Ο: per-observation break-probability model
fragility_encoder.py Frail-Ο with joint encoder fine-tuning / LoRA
sce.py Siamese Counterfactual Encoder (Frail-Ο)
losses.py Huber, triplet, invariance R_inv losses
train.py two-stage SCE training loop
encoders/ hashing, hf_frozen, hf_finetune, hf_lora, openai_embed
baselines/ bertpe, vap3, promptset, pi_adapters, frozen_distance,
amortized_irt, score_predictor, embedding_linear_probe,
length, common
data/ perturbations, math_surface_ops, filler_control,
synth_jobs, synth2_jobs, synth2_perturbations
eval/
estimands.py pi, break events, dose parsing, job targets
deconfound.py cross-fitted residualization (CRD), orthogonality diagnostics
reliability.py parametric-bootstrap reliability ceiling
splits.py fingerprinted job-disjoint train/val/test splits
metrics.py, irt.py, gsmic_adv_deconf.py, reports.py, loading.py
features/ surface features, text featurizers, probes, collision checks
config.py YAML config loader (configs/*.yaml)
llm_env.py, llm_cache.py LLM client configuration + on-disk response cache
schemas.py LabeledPair and friends
scripts/ experiment entry points and runbooks (see Β§7, Β§8)
configs/ 120+ run configurations (suite / dose / backbone / ablation)
release/glassgsm/ GlassGSM download location (hf download reza2ei/GlassGSM; not tracked)
tests/ unit tests (89)
docs/fragility_tuning.md ablation runbook (5 recipe levers)
5. Installation
python -m venv .venv && source .venv/bin/activate
pip install -e ".[hf,openai,dev]" # torch, transformers, sentence-transformers,
# peft, openai, httpx, pytest
# or with uv: uv sync --all-extras
Python β₯ 3.10. The released-data conversion script additionally needs pandas and
pyarrow.
LLM access is needed only for dataset construction, auditing, ex-ante probe
collection, and solver-transfer runs β not for training or evaluating on existing
labeled pairs. Copy .env.example to .env and fill it in. src/causal_difficulty/llm_env.py
selects the backend from LLM_BACKEND (openai | fireworks),
falling back on whichever of LLM_API_KEY / FIREWORKS_API_KEY / OPENAI_API_KEY is
set; point OPENAI_BASE_URL / LLM_BASE_URL at any OpenAI-compatible endpoint.
Hugging Face model weights download automatically on first use.
6. Quickstart (no API access needed)
Rebuild the GlassGSM labeled-pair files from the released parquet (download it first with
hf download reza2ei/GlassGSM --repo-type dataset --local-dir release/glassgsm, which creates
release/glassgsm/data/glassgsm/), then train and evaluate Frail-Ο on a CPU-friendly featurizer:
pip install -e ".[hf,openai,dev]" pandas pyarrow
# 1. released parquet -> data_audited_multiop_mix/*_labeled_pairs.jsonl
python scripts/release_to_labeled_pairs.py --suite glassgsm --solver gpt-4o-mini
# 2. CPU smoke run on the m=9 slice (frozen TF-IDF+SVD features, one seed)
python scripts/fragility_report.py \
--dataset "$PWD/data_audited_multiop_mix/gsmic_adv_m9_multiop_mix_4omini_labeled_pairs.jsonl" \
--featurizer tfidf_svd --seeds 0 --max-epochs 20 \
--out-dir runs/smoke
# 3. unit tests
python -m pytest tests -q
--dataset accepts absolute paths, bare file names resolved under data/, a config
name, a dose-family shorthand (gsmic_adv, math_adv), or a comma-separated list of
files. Relative paths with a directory component are not resolved against the
working directory β use $PWD/.... Step 2 prints the test-fold Spearman for the
break rate and the dose-response parameters and writes report.md / summary.json
into --out-dir. The numbers of this CPU smoke run are not the paper's: the paper's
Frail-Ο uses the fine-tuned encoder on a GPU (Step 5).
7. Reproduction guide
All commands assume the repo root and an activated environment. Dry-run every pipeline first (no API calls):
python scripts/gsmic_adv_pipeline.py --dry-run --max-problems 5 --multi-operator
Step 1 β build twins (per dose, multi-operator)
For each dose m in {1,3,5,7,9} (GlassGSM; GlassMATH is analogous β see
scripts/run_gsmic_full_doses.sh / scripts/run_math_full_doses.sh):
python scripts/gsmic_adv_pipeline.py \
--model gpt-4o-mini \
--max-problems 1319 --max-distractors 20 --distractors-per-twin "$m" \
--multi-operator --surface-twins 10 \
--concurrency 32 \
--cache-dir cache/gsmic_adv_m${m}_4omini \
--output data_multi_full/gsmic_adv_m${m}_multi_4omini_labeled_pairs.jsonl
--multi-operatorgenerates the three families in one pass;--distractors-per-twin $mis the injection size (stacked distractor sentences);--surface-twins 10is the number of twins per surface operator per problem;- responses are cached under
--cache-dir, so re-runs and lower doses are incremental; - GlassMATH:
python scripts/math_adv_pipeline.py --model gpt-4o-mini --no-max-problems --max-distractors 20 --distractors-per-twin $m --multi-operator --surface-twins 10 --output data_math_full/math_adv_m${m}_multi_4omini_labeled_pairs.jsonl(the difficulty-level βcomplexity_scoremapping is built in).
Other solvers/generators are different --model / --backend values; the generator tag
is encoded in the output file suffix (4omini, qwen3p7plus, deepseekv4pro, llama).
Step 2 β No-Op audit
python scripts/audit_contradictions_and_filter.py \
--config gsmic_adv_m${m}_4omini \
--pairs-path data_multi_full/gsmic_adv_m${m}_multi_4omini_labeled_pairs.jsonl \
--out-dir data_audited \
--concurrency 256 --model gpt-4o-mini
Step 3 β build the multi-operator-mix datasets
python scripts/build_multiop_mix.py \
--input data_audited/gsmic_adv_m9_multi_4omini_labeled_pairs.jsonl \
--output data_audited_multiop_mix/gsmic_adv_m9_multiop_mix_4omini_labeled_pairs.jsonl \
--mix-twins 9 --seed 42
Do this for m = 1,3,5,7,9, for GlassMATH, and for other generators. Configs for the
resulting files are configs/{gsmic_adv,math_adv}_m{m}_multiop_mix_{gen}.yaml and the
pooled *_pooled_multiop_mix_*.yaml. (To skip Steps 1β3 for GlassGSM, use the
Quickstart conversion of the released data.)
Step 4 β (optional) ex-ante base probes
The text_probe protocol adds features from repeated samples of the base prompt only
(no twin ever runs β a different, model-access claim):
python scripts/collect_base_probes.py \
--dataset gsmic_adv --model gpt-4o-mini --n-samples 8 \
--out-dir outputs_probes/gsmic_adv
Produces outputs_probes/gsmic_adv/base_probes.json (richer n=24/n=48 variants are the
"richer probes" ablation of docs/fragility_tuning.md). Gate check:
scripts/base_probe_pilot.py.
Step 5 β C1: predict CCD from text (Frail-Ο)
scripts/fragility_report.py is the single headline entry point: it loads the labeled
pairs, makes a fingerprinted job-disjoint three-way split, trains Frail-Ο inside the
process on the train fold only (there is no --checkpoint path that could smuggle in
test-fit weights), early-stops on the validation fold, and reports β over several seeds
β the test-fold Spearman for pi and the dose-response parameters a/b, plus
surface-feature / declared-complexity / measured-Y(J) references and (with
--include-bertpe-vap3) the supervised baselines. Divide by the suite's reliability
ceiling (Step 7) to get the paper's normalized numbers. It also writes the CRD
orthogonality diagnostics (Step 6).
Headline runs, mirroring scripts/run_multiop_mix_conditional_fragility.sh (GPU):
MIX="$PWD/data_audited_multiop_mix"
POOLED="$MIX/gsmic_adv_m1_multiop_mix_4omini_labeled_pairs.jsonl,$MIX/gsmic_adv_m3_multiop_mix_4omini_labeled_pairs.jsonl,$MIX/gsmic_adv_m5_multiop_mix_4omini_labeled_pairs.jsonl,$MIX/gsmic_adv_m7_multiop_mix_4omini_labeled_pairs.jsonl,$MIX/gsmic_adv_m9_multiop_mix_4omini_labeled_pairs.jsonl"
# A) pooled 5-dose (main CCD table) β Frail-Ο = hf_finetune encoder
python scripts/fragility_report.py \
--dataset "$POOLED" --seeds 0,1,2,3,4 --device cuda \
--featurizer hf_finetune --protocol both \
--probe-file outputs_probes/gsmic_adv/base_probes.json \
--include-bertpe-vap3 \
--jobs-per-batch 64 --encoder-microbatch 16 \
--encoder-lr 2e-5 --head-lr 1e-3 --encoder-max-epochs 20 --encoder-patience 5 \
--dump-predictions \
--out-dir runs/multiop_mix/pooled_5dose_hf_finetune
# B) single-dose m=9 (CRD / cross-model protocol)
python scripts/fragility_report.py \
--dataset "$MIX/gsmic_adv_m9_multiop_mix_4omini_labeled_pairs.jsonl" \
--seeds 0,1,2,3,4 --device cuda --featurizer hf_finetune --protocol both \
--probe-file outputs_probes/gsmic_adv/base_probes.json \
--include-bertpe-vap3 --dump-predictions \
--out-dir runs/multiop_mix/m9_only_hf_finetune
Notes:
--protocol text | text_probe | both;text_proberequires--probe-file. The two protocols are reported separately and never merged;--dump-predictionswrites per-seed held-out predictions consumed by the curation, ensembling, and per-family evaluations;- the paper's representation ablation is the
--featurizeraxis; the five training-recipe levers are indocs/fragility_tuning.md.
Step 6 β C2: CRD orthogonality and U-CCD controls
CRD orthogonality (real suites,
Q = pi) comes from the samefragility_report.pyruns: per seed it reportsCorr(target, C),Corr(pred, C),Corr(pred, C^2)(theorthogonality_*block,eval/deconfound.py).U-CCD (Frail-Ο) and the full baseline sweep:
python scripts/paper_supports_real.py --config gsmic_adv_m9_multiop_mix_4omini --seed 7 python scripts/paper_supports_real.py --config gsmic_adv_m9_multiop_mix_4omini \ --estimand d_avg --seed 7 # "the unconditional target is unlearnable"CCD vs CRD across solvers:
scripts/solver_crd_vs_ccd_transfer.py --suite gsmic_adv.
Step 7 β reliability ceilings
Spearman numbers in the paper are normalized by each suite's reliability ceiling (0.969 GlassGSM, 0.972 GlassMATH), computed by parametric bootstrap with a cross-validated empirical-Bayes prior:
python scripts/noise_ceiling.py --config gsmic_adv_m9_4omini --n-repeats 30
python scripts/noise_ceiling.py --config math_adv_m9_4omini --n-repeats 30
Per-slice ceilings: scripts/slice_ceilings.py. Always report the raw Ο_S alongside
normalized values β normalized numbers depend on the ceiling estimator and slice.
Step 8 β placebo / identity-twin floor
Because solver outputs are sampled, B can fire with no true effect. The floor is
measured by re-solving the unperturbed bases of solvable-stratum items:
python scripts/null_twin_noise_floor.py --suite gsmic_adv # paper: 1.1%
python scripts/null_twin_noise_floor.py --suite math_adv # paper: 2.5%
Complementary control: scripts/null_perturbation_control.py --probe-file outputs_probes/gsmic_adv/base_probes.json.
Step 9 β downstream utility: top-k curation
Rank the audited holdout by each predictor and recall truly fragile jobs (pi > 0.05)
at budgets k in {5,10,20,50}%:
# reads Frail-Ο's dumped held-out predictions from the pooled 5-dose run
python scripts/downstream_utility_pi.py
python scripts/downstream_curation.py --config gsmic_adv_m9_4omini --seeds 7 42 123
Step 10 β solver transfer and cross-generator checks
The stratum is defined by the solver's own Y(J), so varying the solver tests how far
the estimand travels:
python scripts/solver_pi_agreement.py \
--configs gsmic_adv_m9_4omini gsmic_adv_m9_qwen3p7plus gsmic_adv_m9_deepseekv4pro
python scripts/cross_model_transfer.py \
--configs gsmic_adv_m9_4omini gsmic_adv_m9_qwen3p7plus gsmic_adv_m9_deepseekv4pro
python scripts/solver_crd_vs_ccd_transfer.py --suite gsmic_adv
Cross-generator checks (Qwen / DeepSeek-authored twins, gpt-4o-mini scoring) reuse
the same entry points on the *_qwen3p7plus / *_deepseekv4pro mix files; BERTPE/VAP3
on those slices: scripts/run_bertpe_vap3_crossmodel.sh.
Step 11 β Synth-CD mechanism checks
python scripts/synth2.py --seed 42 # jobs + twins (offline)
python scripts/simulate2.py # causal DGP labels + oracle D^perp
python scripts/paper_supports_synth2.py --config synth2
python scripts/paper_supports_synth2.py --config synth2_gpt --backbone openai_embed
python scripts/synth_frailpi.py --n-jobs 500 --twins-per-dose 5 --seed 7 \
--out data/synth_frailpi_labeled_pairs.jsonl
python scripts/fragility_report.py --dataset data/synth_frailpi_labeled_pairs.jsonl
python scripts/synth_frailpi_baselines.py
The contrast head should reach the best Spearman with the lowest |rho_C| without
explicit residualization.
Step 12 β baselines
python scripts/run_baselines.py --config gsmic_adv_m9_multiop_mix_4omini
python scripts/run_baselines.py --config math_adv_m9_multiop_mix_4omini
The PromptSET / frozen-geometry baselines retargeted to the break-event pi:
scripts/run_missing_pi_baselines.py. Per-family baseline table:
scripts/ccd_per_family_baselines.py.
Step 13 β per-category / per-family breakdowns
python scripts/ccd_per_category.py --files "$POOLED" --label gsmic_adv
python scripts/ccd_per_family.py --files "$POOLED" --label gsmic_adv
python scripts/ccd_per_family_predict.py ... # evaluate dumped predictions
8. Orchestration scripts
Shell runbooks that chain the entry points above as the paper's runs were launched (multi-GPU where noted; run from the repository root):
| Script | What it runs |
|---|---|
run_multiop_mix_conditional_fragility.sh |
headline C1+C2: pooled 5-dose + m=9 Frail-Ο, 5 seeds, BERTPE/VAP3 |
run_multiop_mix_m9_full_baselines.sh |
m=9 full baseline sweep (paper_supports_real.py) |
run_multiop_mix_pooled_sce.sh |
pooled 5-dose Frail-Ο (d_avg/d_wc) + baselines |
run_multiop_mix_sce_dosage_sweep.sh |
all doses Γ 5 seeds, Frail-Ο + baselines |
run_multiop_mix_frailpi_contrast_ablation.sh |
--lambda-contrast sweep |
run_multiop_mix_lambdainv_ablation.sh |
invariance-regularizer lambda_inv sweep |
run_multiop_mix_richer_probes.sh |
richer ex-ante probes (n=24) ablation |
run_fragility_tuning_all.sh + docs/fragility_tuning.md |
5 recipe levers (RoBERTa-large, lr/batch, pooling, β¦) |
run_gsmic_full_doses.sh / run_math_full_doses.sh |
full per-dose twin generation + audit |
run_bertpe_vap3_crossmodel.sh |
BERTPE/VAP3 on cross-generator slices |
run_train_eval_synthetic_3seeds.sh |
Synth-CD train/eval |
run_c2_diagnostics.sh |
C2 diagnostics on pi / surface-proxy D^perp |
scripts/ also holds many one-off launchers, resume scripts, and analysis helpers
from the experiment campaign; the table above lists the ones tied to paper results.
9. Configuration reference
YAML configs in configs/, loaded by name via causal_difficulty.config.load_config.
A mix-corpus config (configs/gsmic_adv_m9_multiop_mix_4omini.yaml):
seed: 42
paths:
data_dir: data_audited_multiop_mix
outputs_dir: outputs/gsmic_adv_m9_multiop_mix_4omini
cache_dir: cache/gsmic_adv_m9_multiop_mix_4omini
labeled_pairs: data_audited_multiop_mix/gsmic_adv_m9_multiop_mix_4omini_labeled_pairs.jsonl
dataset:
n_jobs: 200
twins_per_job: 20
encoder:
backbone: openai_embed # hashing | hf_frozen | hf_finetune | hf_lora | openai_embed
hf_frozen: { model: BAAI/bge-small-en-v1.5, batch_size: 256 }
openai_embed: { model: text-embedding-3-small, max_concurrency: 16 }
model:
hidden_dim: 64
complexity_dim: 32
contrast_context: surface_pair # surface_pair | pair | none
dropout: 0.25
ex_ante_n_layers: 2
train:
representation: { epochs: 200, lr: 0.001, batch_size: 64, triplet_margin: 0.5 }
calibration: { epochs: 1000, lr: 0.0001, weight_decay: 0.01, batch_size: 32,
lambda_inv: 0.0, huber_delta: 0.1 }
eval:
fragile_threshold: 0.05
holdout_frac: 0.3
stratified_split: true
Frail-Ο's recipe is set by the fragility_report.py
flags shown in Step 5 (--encoder-lr 2e-5 --head-lr 1e-3 --hidden-dim 256 ...); the
model: / train: blocks above configure the Frail-Ο / SCE path.
10. Outputs and tests
runs/{run_id}/β one directory per experiment:report.md(human-readable tables),summary.json(full metrics, per seed), prediction dumps (--dump-predictions), logs; a one-line summary is appended toruns/runs.jsonl.outputs_fragility/{dataset}/β defaultfragility_report.pyoutput directory.outputs_probes/{suite}/base_probes.jsonβ ex-ante probe features.data_audited*/,data_audited_multiop_mix/β audited benchmarks and the mix corpus (built or converted, not committed).cache/β LLM response caches (safe to delete; re-runs re-call the API).
These directories and model checkpoints are git-ignored.
python -m pytest tests -q # 89 tests: estimands, deconfound, collision audit,
# filler control, math-adv, human audit, units
11. Reproducibility notes
- Stratum membership is a measured quantity.
Y(J)is the modal answer of 5 samples at temperature 0.2; the identity-twin floor (Step 8) quantifies the re-sampling noise that leaks intopi. - Doses are stamped from filenames (
_m{k}_); the legacym=1math_irrelevant_contexttag (no suffix) is normalized at load time. - Splits are fingerprinted and job-disjoint across train/val/test; checkpoints carry
a sidecar of their train job-ids and evaluators refuse to score a checkpoint that
overlaps their holdout. Use the same
--seedsand--datasetstring when comparing featurizers β folds are deterministic in(--dataset, seed). - Ceiling-normalized numbers are estimator-sensitive. The paper's 0.969 / 0.972 come
from the parametric-bootstrap estimator of Step 7 on the audited corpora behind the
tables. Re-running on a different slice (single dose vs pooled), probe file, or
estimator prior changes both the ceiling and the achievable Ο_S; compare raw Ο_S on
identical splits, and expect absolute values from re-runs to differ from the paper's
tables, especially for the single-dose
m = 9slice (the paper's ownm = 9solver-transfer diagonal is 0.431 on GlassGSM). - The ex-ante probe file matters.
text_proberesults depend on the probe sample count and draw (--n-samples); the probe file behind the paper's runs is not part of this repository. - Complexity proxies are declared and external; CRD is orthogonal to that proxy
only. For GlassGSM the released pipeline computes
complexity_scorefrom the problem text (question numbers, keywords, sentences), which correlates with length. For GlassMATH it is the problem-setter level. - Per-family structure. Each mix-corpus twin applies a single operator. On the
released GlassGSM data (
gpt-4o-mini, unique twins of solved bases) the break rate is 13.8% for sentence reordering, 13.0% for distractor injection, and 1.5% for entity renaming β the last is close to the 1.1% identity-twin floor, so renaming is a low-signal family and most of the fragility comes from reordering and injection. Per-family breakdowns: Step 13.
12. Citation, license, third-party material
@misc{rezaei2026fragilityreduciblehardnessconditional,
title={Fragility Is Not Reducible to Hardness: Conditional Causal Difficulty for LLMs},
author={Mohammad Reza Rezaei and Dingxian Wang and Nikhil Verma and Sajjad Pakdamansavoji and Andrew Rabinovich and Zhao Chen},
year={2026},
eprint={2611.02065},
archivePrefix={arXiv},
primaryClass={cs.LG},
url={https://arxiv.org/abs/2611.02065},
}
The package is released under the MIT license (pyproject.toml). GlassGSM / GlassMATH
contain text derived from GSM8K (MIT, OpenAI) and the Hendrycks MATH benchmark (MIT);
see NOTICE.md in the dataset repos for the third-party notices and license text.