Skip to content

Commit 462f4b3

Browse files
author
Drew Prinster {External}
committed
documenting DPO scoring for infeasible prompt sequences notes, editing readme
1 parent 102e4b8 commit 462f4b3

3 files changed

Lines changed: 48 additions & 8 deletions

File tree

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Conformal Policy Control
22

3-
Code for ["Conformal Policy Control"](https://arxiv.org/abs/2603.02196) (ICML 2026 spotlight paper): a framework for enabling AI agents to automatically determine their own "zone of competence," where we can guarantee their behavior will respect a user's risk tolerance, $\alpha$.
3+
Code for ["Conformal Policy Control"](https://arxiv.org/abs/2603.02196) (ICML 2026 spotlight paper): a framework for enabling AI agents to automatically determine their own "zone of competence," where we can place guarantees on their behavior respecting a user's risk tolerance, $\alpha$.
44

55
By Drew Prinster, Clara Fannjiang, Ji Won Park, Kyunghyun Cho, Anqi Liu, Suchi Saria, and Samuel Stanton.
66

@@ -18,26 +18,28 @@ If you use this code, please our paper:
1818

1919
## Overview
2020

21-
This project develops **Conformal Policy Control (CPC)**: a method for iteratively improving a language model policy while maintaining formal guarantees on the risk (e.g., rate of infeasible or unsafe outputs) over time. The key idea is to constrain each optimized policy's likelihood ratios relative to a safe reference policy, with the constraint level calibrated via conformal prediction so that risk stays below a user-specified level alpha.
21+
This project develops **Conformal Policy Control (CPC)**: a method for iteratively improving a language model policy while maintaining formal guarantees on the risk (e.g., rate of infeasible or unsafe outputs) over time. The key idea is to constrain each optimized policy's likelihood ratios relative to a safe reference policy, with the constraint level calibrated via CPC so that risk stays below a user-specified level, $\alpha$.
2222

2323
![CPC animation: search and sampling](visuals/Animation_alpha0.5_betaHat10.400000_CPCsearchTrue_samplingTrue_proposalsTrue.gif)
2424

2525
The repository contains four sets of experiments:
2626

27-
- **`cpc_llm/`** : The main CPC pipeline for LLMs, applied to the Ehrlich function protein discovery task ([Chen, et al. 2025](https://arxiv.org/abs/2410.22296)). Pre-trains a LM on data from a genetic algorithm, then iteratively generates and scores new samples, trains optimized policies (SFT, DPO, or MARGE), and uses CPC to ensure the improved policies satisfy safety constraints.
28-
- **`cbo/`** : Constrained Bayesian optimization experiments (in paper appendix). Compares CPC to classic conservative optimization. Simplest initial entrypoint to CPC, runs on single CPU.
27+
- **`cpc_llm/`** : **The main CPC pipeline for LLMs**, applied to the Ehrlich function synthetic protein discovery task ([Chen, et al. 2025](https://arxiv.org/abs/2410.22296)). Pre-trains a language model on data from a genetic algorithm, then iteratively generates and scores new samples, trains optimized policies (SFT, DPO, or MARGE), and uses CPC to ensure the improved policies satisfy safety constraints.
28+
- **`cbo/`** : Constrained Bayesian optimization experiments (in paper appendix). Compares CPC to classic conservative optimization. **This is a more accessible initial entrypoint to CPC code (runs on a single CPU).**
2929
- **`constrained_AL/`** : CPC constrained active learning with Gaussian process surrogates, applied to tabular regression benchmarks.
3030
- **`QA_expts/`** : Generalized conformal risk control (gCRC) for LLM factuality, controlling false discovery rate (a non-monotonic loss) on medical QA dataset of GPT-3.5-Turbo responses.
3131

3232
## Setup
3333

3434
Requires Python >= 3.10 and [uv](https://docs.astral.sh/uv/).
3535

36+
To install all dependencies (including the `cpc-llm` package in editable mode) and dev tools (pytest), and then activate the environment, run
37+
3638
```bash
3739
uv sync --group dev
38-
```
3940

40-
This installs all dependencies (including the `cpc-llm` package in editable mode) and dev tools (pytest).
41+
source .venv/bin/activate
42+
```
4143

4244
## Running the CPC-LLM pipeline
4345

@@ -52,6 +54,9 @@ cpc-llm --config-name=cpc_llm \
5254
conformal_policy_control.alpha=0.6 initial_seed=0 last_seed=0 \
5355
local_output_dir=/path/to/local \
5456
parent_output_dir=s3://bucket/path
57+
58+
# With slurm (and changing resource params in run_cpc_llm.sh), run many parallel GPU jobs to reproduce paper's Fig 6 via
59+
bash submit_cpc_llm_expts.sh "0.4,0.6,0.8,1.0" 0 29
5560
```
5661

5762

@@ -60,15 +65,16 @@ cpc-llm --config-name=cpc_llm \
6065

6166
| Parameter | Description |
6267
|-----------|-------------|
63-
| `conformal_policy_control.alpha` | Risk level (e.g., 0.1 for 10% constraint violation rate) |
68+
| `conformal_policy_control.alpha` | Risk level (e.g., 0.4 for 40% constraint violation rate) |
6469
| `num_sft_rounds` / `num_dpo_rounds` / `num_marge_rounds` | Number of training iterations per method |
70+
| `initial_seed` / `last_seed` | Initial / last random seeds (inclusive) to run in loop for repeat trials |
6571
| `parent_output_dir` | S3 path for outputs (set to `null` for local-only) |
6672
| `local_output_dir` | Local path for outputs and model checkpoints |
6773

6874
### Important notes
6975

7076
- **Storage**: The pipeline supports both local and S3 storage. Model checkpoints are copied to S3 and deleted locally after training. Set `parent_output_dir: "null"` to disable S3.
71-
- **SLURM**: Training, generation, and scoring jobs are launched as SLURM jobs. Configure via `slurm_args` sections in the config.
77+
- **SLURM**: Training, generation, and scoring jobs are launched as their own SLURM jobs. Configure via `slurm_args` sections in the config.
7278
- **Resuming**: The pipeline automatically resumes prior runs if launched with the same config. Use `--overwrite=True` to start fresh.
7379
- **GPU requirements**: Training uses DDP (single-node multi-GPU). You need ~4x the model size in GPU RAM for full-precision training.
7480

@@ -94,6 +100,7 @@ cpc_llm/ # Main CPC-LLM package (installable)
94100
infrastructure/ # File handling (local/S3), orchestration, SLURM
95101
train/ # SFT, DPO, MARGE training
96102
test_functions/ # Ehrlich benchmark utilities
103+
cbo/ # Constrained Bayesian optimization experiments
97104
constrained_AL/ # Active learning experiments
98105
QA_expts/ # Medical QA experiments
99106
notebooks/ # Visualization notebooks
@@ -103,3 +110,7 @@ tests/ # Unit tests
103110
## License
104111

105112
See [LICENSE](LICENSE).
113+
114+
## Known implementation notes
115+
116+
- **DPO scoring for infeasible prompt sequences**: `DPOTrainerWithLogging` does not count a prompt→response transition from infeasible to feasible as a score improvement, which makes DPO training more permissive of infeasible outputs than originally intended. This does not invalidate the paper's experiments (its main effect is increasing the risk of the unconstrained policy), so it is kept as-is for reproducibility. See the full explanation in [`pref_tuning_trainer.py`](cpc_llm/src/cpc_llm/train/pref_tuning_trainer.py#L123-L141).

cpc_llm/src/cpc_llm/data/synthetic_dataset_formatter.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,14 @@ def find_preference_pairs(cfg: DictConfig, df: pd.DataFrame) -> List[Dict[str, A
425425
# comparisons against NaN are always False, which would silently produce
426426
# zero pairs instead of treating the particle as worst-possible.
427427
scores_np = filtered_scores.numpy()
428+
429+
## Note on DPO scoring for infeasible prompt sequences:
430+
## The following commented out line "scores_np = np.where(np.isnan(scores_np), np.inf, scores_np)"
431+
## relates to the "Note on DPO scoring for infeasible prompt sequences" in ../train/pref_tuning_trainer.py.
432+
## Please see the note in that file.
433+
434+
# scores_np = np.where(np.isnan(scores_np), np.inf, scores_np)
435+
428436
pynn_transformer = PyNNDescentTransformer(
429437
n_neighbors=cfg.n_neighbors, metric=cfg.distance_metric
430438
).fit(filtered)

cpc_llm/src/cpc_llm/train/pref_tuning_trainer.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ def get_metrics_for_outputs(
119119
num_repeated_input += 1
120120
particle = torch.FloatTensor(particle).unsqueeze(0)
121121
score = self.test_fn(particle).item()
122+
123+
## Note on DPO scoring for infeasible prompt sequences:
124+
## The following if statement patches a NaN/None handling bug caused by inf scores
125+
## (for infeasible sequences) being recorded as NaN/None. The original intention was to increment
126+
## num_decreased_score also in the case that the prompt was infeasible (i.e., input_scores[i]
127+
## is None or inf) and the response was feasible (i.e., score is not None nor inf). However, the
128+
## current implemented behavior does *not* increment in this case, which essentially makes the
129+
## DPO training more "unsafe" / less incentivized to avoid infeasible outputs, which makes
130+
## for a setting where the effect of CPC "reining in" an unsafe model can be demonstrated.
131+
## That is, this "bug" makes for a less standard DPO training, but does *not* invalidate
132+
## the experiments, so it is left as-is for reproducibility. The following commented-out
133+
## code could be used for intended DPO behavior, but then to reproduce the paper's qualitative
134+
## reward findings (where CPC could improve reward), additional hyperparameter tuning may be required,
135+
## for instance increasing DPO learning rate to make it a bit overly aggressively optimized.
136+
## In addition to this line, the commented-out line "scores_np = np.where(np.isnan(scores_np), np.inf, scores_np)"
137+
## in ../data/synthetic_dataset_formatter.py should also be incorporated.
138+
139+
# if input_scores[i] is None and (score is not None and score != float("inf")):
140+
# num_decreased_score += 1
141+
# elif input_scores[i] is not None and score < input_scores[i]:
142+
122143
if (input_scores[i] is not None and score is not None) and score < input_scores[i]:
123144
num_decreased_score += 1
124145
if score == float("inf"):

0 commit comments

Comments
 (0)