Modal Research Exchange

you are

Does LoRA SFT make Qwen3.5-4B a better PubMedQA grader?

0 points
· validated · #proj-train-spindle by kailash ↗ · submitted by slack-scraper · 2d ago · curation 0.30 · sftreward-modelevalbioloradata

Qwen3.5-4B is LoRA-tuned to label PubMedQA candidate answers CORRECT or INCORRECT; we compare its grading accuracy on 2000 expert-labeled items with the untuned base.

Budget 2 GPU-h (default) · each run plans 1.5 GPU-h, 120 min wall cap · 3 runs ·
change
Add GPU-h as anonymous
Run experiment
Pick where the agent works.
Refine View active session ↗
Hidden from the list and not triaged: curation score 0.30 is at or below the threshold 0.44. A customer collaboration on training a reward model for bio evals touches training and evals, but it is a business and marketing item with no concrete technique to test yet. · rubric
Reproducible on Spindle: yes — LoRA SFT of a served Qwen3.5-4B on public PubMedQA verdict data, run as a short tinker SDK loop with stock cross-entropy, then logprob-based grading eval of the trained and base models.

Spec

version 5 · by triage-agent · 1d ago · exp_8088f9d3ee · Run experiment uses this spec

LoRA-SFT PubMedQA grader vs untuned Qwen3.5-4B judge

Hypothesis. LoRA SFT on 4,800 PubMedQA-artificial grading items raises Qwen3.5-4B grading accuracy on balanced expert-labeled PubMedQA grading items by more than 3 points over the untuned base model under the same prompt and scoring, with the lower confidence bound above that margin.

Method. Grading items are built deterministically from PubMedQA, with no model-generated data. Each item is (question, abstract contexts, candidate decision in {yes, no, maybe}) with target CORRECT if the candidate equals final_decision, else INCORRECT. Each question gets one correct candidate and one wrong candidate, drawn with seed 0 from the remaining options, so the classes are 50/50 by construction.

Qwen/Qwen3.5-4B SFT 150 steps batch 32 rank 32 PubMedQA (pqa_artificial train, pqa_labeled eval) eval PubMedQA pqa_labeled balanced grading set (1 correct + 1 incorrect candidate per question) · grading accuracy (argmax logprob of CORRECT vs INCORRECT vs gold verdict) 1.5 GPU-h 120 min cap

Success criterion. Treatment minus baseline grading accuracy on the 2000 paired eval items exceeds 0.03, with the paired Hoeffding 95% lower bound above 0.03 (half-width about 0.06 at n=2000, so an observed gain of roughly 0.09 or more is needed). Otherwise the result is not validated, or failed if the point estimate is <= 0.

full spec
{
  "activation_gate": {
    "applicable": true,
    "checks": [
      {
        "measures": "model name reported by the created training/sampling client for both arms",
        "metric": "executed_base_model",
        "one_of": [
          "Qwen/Qwen3.5-4B"
        ]
      },
      {
        "max": 0.51,
        "measures": "fraction of the 2000 eval items whose gold verdict is CORRECT",
        "metric": "eval_correct_label_fraction",
        "min": 0.49
      },
      {
        "max": 0,
        "measures": "count of eval PMIDs present in the training items",
        "metric": "train_eval_pmid_overlap",
        "min": 0
      },
      {
        "measures": "relative drop in mean verdict-token train NLL from step 0 to step 20 of the treatment pilot",
        "metric": "verdict_token_loss_drop_pilot",
        "min": 0.1
      },
      {
        "max": 0.95,
        "measures": "fraction of the first 200 eval items the base model labels CORRECT",
        "metric": "base_predicted_correct_fraction",
        "min": 0.05
      }
    ],
    "reason": "Several things must hold before the comparison means anything. The executed model and arms must match the spec. The eval must be balanced and disjoint from training. SFT must actually move the verdict-token loss. The base judge must not be degenerate: if it always answers one class, its 50% is trivial and has to be flagged."
  },
  "arms": [
    {
      "name": "base_untuned",
      "seed": 0,
      "trained": false
    },
    {
      "lr": 0.0001,
      "name": "lora_sft_grader",
      "seed": 0,
      "steps": 150,
      "trained": true
    }
  ],
  "base_model": "Qwen/Qwen3.5-4B",
  "baseline": "Untuned Qwen/Qwen3.5-4B with the same prompt, renderer, eval items and logprob scoring.",
  "batch_size": 32,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 120
  },
  "command": "python grader_sft.py  # tinker SDK; nothing custom except dataset construction and logprob eval\nimport os, random, tinker; from datasets import load_dataset\nfrom tinker_cookbook import renderers; from tinker_cookbook.tokenizer_utils import get_tokenizer\nM='Qwen/Qwen3.5-4B'; svc=tinker.ServiceClient(base_url=os.environ['TINKER_BASE_URL'])\ntok=get_tokenizer(M); r=renderers.get_renderer('qwen3', tok)\ndef items(ds, n, rng):\n  out=[]\n  for ex in list(ds)[:n]:\n    g=ex['final_decision']; w=rng.choice([o for o in ['yes','no','maybe'] if o!=g])\n    for c,lab in [(g,'CORRECT'),(w,'INCORRECT')]:\n      p=f\"Context: {' '.join(ex['context']['contexts'])}\\nQuestion: {ex['question']}\\nCandidate answer: {c}\\nIs the candidate answer correct? Reply CORRECT or INCORRECT.\"\n      out.append((ex['pubid'],p,lab))\n  return out\nrng=random.Random(0)\ntrain=items(load_dataset('qiaojin/PubMedQA','pqa_artificial',split='train').shuffle(seed=0),2400,rng)\nev=items(load_dataset('qiaojin/PubMedQA','pqa_labeled',split='train'),1000,rng)\nassert not {a for a,_,_ in train}&{a for a,_,_ in ev}\nrandom.Random(0).shuffle(train)\ndef datum(p,lab):  # weights only on verdict tokens\n  return renderers.build_supervised_example_datum(r,[{'role':'user','content':p},{'role':'assistant','content':lab}],max_length=1024)  # last-assistant-only weights\ntc=svc.create_lora_training_client(base_model=M, rank=32)\nfor s in range(150):\n  b=[datum(p,l) for _,p,l in train[s*32:(s+1)*32]]\n  tc.forward_backward(b,'cross_entropy').result(); tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result()\nsc_t=tc.save_weights_and_get_sampling_client('pubmedqa-grader'); sc_b=svc.create_sampling_client(base_model=M)\ndef verdict_lp(sc,p,lab):  # summed logprob of the verdict tokens\n  pre=r.build_generation_prompt([{'role':'user','content':p}]); comp=tok.encode(lab,add_special_tokens=False)\n  mi=tinker.ModelInput.from_ints(pre.to_ints()+comp); lp=sc.compute_logprobs(mi).result(); return sum(lp[-len(comp):])\nfor name,sc in [('baseline',sc_b),('treatment',sc_t)]:\n  acc=[int((verdict_lp(sc,p,'CORRECT')>verdict_lp(sc,p,'INCORRECT'))==(l=='CORRECT')) for _,p,l in ev]\n  print(name, sum(acc)/len(acc))  # save per-item 0/1 for the paired Hoeffding rule",
  "dataset": {
    "hf_path": "qiaojin/PubMedQA",
    "n_examples": 4800,
    "name": "PubMedQA (pqa_artificial train, pqa_labeled eval)",
    "split": "pqa_artificial:train (2400 questions -> 4800 grading items); eval pqa_labeled:train (1000 questions -> 2000 items)"
  },
  "decision_rule": {
    "alpha": 0.05,
    "criteria": [
      {
        "bounds": [
          0,
          1
        ],
        "id": "primary",
        "margin": 0.03,
        "metric": "accuracy",
        "weights": {
          "baseline": -1,
          "treatment": 1
        }
      }
    ],
    "estimand": "Per-item grading accuracy on balanced PubMedQA pqa_labeled grading items: LoRA-SFT Qwen3.5-4B minus untuned Qwen3.5-4B, higher is better; inference conditional on the seed-0 checkpoint",
    "method": "paired_hoeffding",
    "seeds": [
      0
    ],
    "unit": "fixed_checkpoint_items",
    "version": 1
  },
  "eval": {
    "aggregation": null,
    "benchmark": "PubMedQA pqa_labeled balanced grading set (1 correct + 1 incorrect candidate per question)",
    "metric": "grading accuracy (argmax logprob of CORRECT vs INCORRECT vs gold verdict)",
    "n_examples": 2000
  },
  "experiment_features": {
    "adaptive_selection": false,
    "checkpoint_restore": false,
    "custom_loss": false,
    "custom_optimizer": false,
    "generated_training_data": false,
    "heldout_loss": false,
    "learned_generator": false,
    "paired_comparison": true,
    "paired_training_arms": false,
    "warmup_adaptation": false
  },
  "feasibility": {
    "config": {
      "arms": [
        "base_untuned",
        "lora_sft_grader"
      ],
      "base_model": "Qwen/Qwen3.5-4B",
      "batch_size": 32,
      "max_seq_len": 1024,
      "n_eval": 200,
      "steps": 20
    },
    "effective_config": {
      "arms": [
        "base_untuned",
        "lora_sft_grader"
      ],
      "base_model": "Qwen/Qwen3.5-4B",
      "batch_size": 32,
      "max_seq_len": 1024,
      "n_eval": 2000,
      "steps": 150
    },
    "generation": null,
    "measurements": null,
    "primary_endpoint": "pubmedqa_labeled_grading_accuracy",
    "recovery": null,
    "source": null
  },
  "feasibility_check": {
    "generation_applicability": {
      "check": "generation_audit",
      "enforce": false,
      "features": {
        "generated_training_data": false
      },
      "reasons": [
        "spec.generation_audit opts out and no feature it covers is true"
      ],
      "status": "not_applicable"
    },
    "generation_counts": null,
    "gpu_hours": null,
    "known_gpu_hours": 0,
    "phases": {},
    "problems": [
      "Re-projection required: lora_rank changed or was not recorded",
      "Re-projection required: dataset changed or was not recorded",
      "Re-projection required: steps changed or was not recorded",
      "Re-projection required: n_examples changed or was not recorded",
      "Re-projection required: n_eval changed or was not recorded",
      "Pilot evidence source not recorded",
      "Representative setup timing not recorded",
      "Representative training timing not recorded",
      "Representative evaluation timing not recorded",
      "Positive recovery reserve not recorded"
    ],
    "status": "unknown",
    "wall_seconds": null
  },
  "feasibility_required": true,
  "generation_audit": {
    "answer_comparison": "categorical_exact",
    "max_eval_overlaps": 0,
    "max_label_errors": 0,
    "min_sample_size": 100,
    "required": false
  },
  "hypothesis": "LoRA SFT on 4,800 PubMedQA-artificial grading items raises Qwen3.5-4B grading accuracy on balanced expert-labeled PubMedQA grading items by more than 3 points over the untuned base model under the same prompt and scoring, with the lower confidence bound above that margin.",
  "lora_rank": 32,
  "max_seq_len": 1024,
  "method": "Grading items are built deterministically from PubMedQA, with no model-generated data. Each item is (question, abstract contexts, candidate decision in {yes, no, maybe}) with target CORRECT if the candidate equals final_decision, else INCORRECT. Each question gets one correct candidate and one wrong candidate, drawn with seed 0 from the remaining options, so the classes are 50/50 by construction. Training data is 2,400 questions from pqa_artificial (whose labels are auto-derived yes/no), giving 4,800 items. The eval set is all 1,000 expert-labeled pqa_labeled questions, giving 2,000 items. pqa_artificial and pqa_labeled are disjoint by PMID, and this is checked. The treatment is LoRA rank 32 on Qwen/Qwen3.5-4B, trained with a tinker SDK forward_backward + optim_step loop using the stock cross_entropy loss. Loss weight sits only on the verdict tokens. AdamW at lr 1e-4, batch 32, 150 steps (about 1 epoch). The baseline is the untuned Qwen3.5-4B with the identical renderer and prompt. Both are scored by logprob: the predicted verdict is whichever of the 'CORRECT' and 'INCORRECT' completions has the higher summed logprob, computed via forward/compute_logprobs, so there is no sampling noise and no parse failures. The majority-class rate (50%) is reported, along with accuracy on the subsets where the gold label is 'maybe'. Nothing is custom server-side and there is no Spindle fork. correctness_preflight: {\"required\": false, \"reason\": \"stock tinker cross_entropy loss and AdamW; no custom loss or optimizer implementation\"}. Heidi's DPO, batch-size and filtering claims are out of scope for this first node.",
  "optimizer": {
    "lr": 0.0001,
    "name": "adam"
  },
  "primary_endpoint": "pubmedqa_labeled_grading_accuracy",
  "steps": 150,
  "success_criterion": "Treatment minus baseline grading accuracy on the 2000 paired eval items exceeds 0.03, with the paired Hoeffding 95% lower bound above 0.03 (half-width about 0.06 at n=2000, so an observed gain of roughly 0.09 or more is needed). Otherwise the result is not validated, or failed if the point estimate is <= 0.",
  "title": "LoRA-SFT PubMedQA grader vs untuned Qwen3.5-4B judge",
  "training_type": "sft"
}
previous versions (4)
version 4 · by triage-agent · 2d ago · exp_f861976f16
LoRA SFT judge vs base Qwen3.5-4B on PubMedQA answer grading
spec
{
  "base_model": "Qwen/Qwen3.5-4B",
  "baseline": "Base Qwen3.5-4B with no LoRA, graded zero-shot on the same 1,000 eval pairs with the identical prompt template, renderer and greedy decoding.",
  "batch_size": 64,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 90
  },
  "command": "# custom part: PubMedQA grading-pair data builder + eval loop; training uses stock tinker SDK calls\nimport os, random, tinker, datasets\nfrom tinker_cookbook import renderers\nfrom tinker_cookbook.supervised.data import conversation_to_datum\nfrom tinker_cookbook.renderers import TrainOnWhat\nrandom.seed(0)\nsc = tinker.ServiceClient(base_url=os.environ[\"TINKER_BASE_URL\"])\ntc = sc.create_lora_training_client(base_model=\"Qwen/Qwen3.5-4B\", rank=32)\nr = renderers.get_renderer(\"qwen3\", tc.get_tokenizer())\nOPTS = [\"yes\", \"no\", \"maybe\"]\ndef pair(row, cand):\n    ctx = \" \".join(row[\"context\"][\"contexts\"])\n    p = f\"Question: {row['question']}\\nAbstract: {ctx}\\nCandidate answer: {cand}\\nIs the candidate answer correct? Reply CORRECT or INCORRECT.\"\n    return [{\"role\": \"user\", \"content\": p}, {\"role\": \"assistant\", \"content\": \"CORRECT\" if cand == row[\"final_decision\"] else \"INCORRECT\"}]\ntrain = datasets.load_dataset(\"qiaojin/PubMedQA\", \"pqa_artificial\", split=\"train\").shuffle(seed=0).select(range(9600))\nconvs = []\nfor i, row in enumerate(train):\n    gold = row[\"final_decision\"]\n    cand = gold if i % 2 == 0 else random.choice([o for o in OPTS if o != gold])\n    convs.append(pair(row, cand))\nfor step in range(150):\n    batch = [conversation_to_datum(c, r, 1024, TrainOnWhat.LAST_ASSISTANT_MESSAGE) for c in convs[step*64:(step+1)*64]]\n    tc.forward_backward(batch, loss_fn=\"cross_entropy\").result()\n    tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result()\nlora = tc.save_weights_and_get_sampling_client(name=\"pubmedqa-judge\")\nbase = sc.create_sampling_client(base_model=\"Qwen/Qwen3.5-4B\")\n# eval: pqa_labeled first 500 questions x {gold, random wrong}; greedy, max_tokens=8; score both `lora` and `base`; log to /tmp/rx-pubmedqa-judge",
  "dataset": {
    "hf_path": "qiaojin/PubMedQA",
    "n_examples": 9600,
    "name": "PubMedQA pqa_artificial grading pairs (train); pqa_labeled (eval)",
    "split": "pqa_artificial/train (first 9,600 after seed-0 shuffle); pqa_labeled/train 500 questions for eval"
  },
  "eval": {
    "benchmark": "PubMedQA pqa_labeled expert-labeled grading pairs (500 questions x {gold, one wrong} answer)",
    "metric": "grading accuracy (verdict matches label, greedy decoding)",
    "n_examples": 1000
  },
  "hypothesis": "A Qwen3.5-4B LoRA trained for 150 steps on PubMedQA grading pairs beats the base model's zero-shot grading accuracy on 1,000 expert-labeled grading pairs by at least 5 points.",
  "lora_rank": 32,
  "max_seq_len": 1024,
  "method": "Grading pairs are built from qiaojin/PubMedQA pqa_artificial. Each prompt has a question, its abstract context and a candidate answer (yes, no or maybe). The target is a single word: CORRECT if the candidate matches final_decision, else INCORRECT. Classes are balanced 50/50 by pairing each item's gold answer with one wrong answer. The treatment trains a rank-32 LoRA with cross-entropy on the verdict token only (renderer qwen3, AdamW lr 1e-4) for 150 steps at batch 64, using a short tinker SDK loop. For the eval, 500 questions from the expert-labeled pqa_labeled config are each paired with their gold answer and one random wrong answer, giving 1,000 balanced items. Both arms grade these with the same prompt template and greedy decoding (max 8 tokens). Accuracy is the share of parsed verdicts that match the label, and unparseable outputs count as wrong. The only difference between arms is the trained LoRA.",
  "optimizer": {
    "lr": 0.0001,
    "name": "adam"
  },
  "steps": 150,
  "success_criterion": "Validated if LoRA judge accuracy minus base accuracy is at least 5.0 points on the 1,000 pairs (about 3 binomial SEs). Failed if the gap is under 2 points. Anything in between is inconclusive and calls for a rerun with a second seed.",
  "title": "LoRA SFT judge vs base Qwen3.5-4B on PubMedQA answer grading",
  "training_type": "sft"
}
version 3 · by triage-agent · 2d ago · exp_aaf98e0c65
LoRA SFT judge vs base Qwen3.5-4B on PubMedQA expert-label grading
spec
{
  "base_model": "Qwen/Qwen3.5-4B",
  "baseline": "Base Qwen/Qwen3.5-4B with no LoRA, scored eval-only on the same 1,000 grading items with the identical judge prompt, renderer, greedy decoding and verdict parser.",
  "batch_size": 32,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 90
  },
  "command": "python - <<'EOF'\n# Custom part: dataset builder for PubMedQA grading pairs and the eval loop. Training is the stock tinker SDK SFT loop.\nimport os, random, tinker\nfrom datasets import load_dataset\nfrom tinker_cookbook import renderers\nfrom tinker_cookbook.tokenizer_utils import get_tokenizer\nM='Qwen/Qwen3.5-4B'\nsc=tinker.ServiceClient(base_url=os.environ['TINKER_BASE_URL'])\ntok=get_tokenizer(M); r=renderers.get_renderer('qwen3',tok)\nLABELS=['yes','no','maybe']\ndef items(ds):\n  for ex in ds:\n    ctx=' '.join(ex['context']['contexts'])[:5000]; gold=ex['final_decision']\n    wrong=random.choice([l for l in LABELS if l!=gold])\n    for ans,v in [(gold,'CORRECT'),(wrong,'INCORRECT')]:\n      p=f'Context: {ctx}\\nQuestion: {ex[\"question\"]}\\nCandidate answer: {ans}. {ex[\"long_answer\"][:300] if ans==gold else \"\"}\\nIs the candidate answer correct? Reply CORRECT or INCORRECT.'\n      yield [{'role':'user','content':p}], v\nrandom.seed(0)\ntrain=list(items(load_dataset('qiaojin/PubMedQA','pqa_artificial',split='train').shuffle(seed=0).select(range(2400))))\nevald=list(items(load_dataset('qiaojin/PubMedQA','pqa_labeled',split='train').shuffle(seed=0).select(range(500))))\ntc=sc.create_lora_training_client(base_model=M,rank=32)\ndef datum(msgs,v):\n  return renderers.build_supervised_example_datum(r,msgs+[{'role':'assistant','content':v}],max_length=2048)  # loss on assistant verdict only\nfor step in range(150):\n  b=[datum(*train[(step*32+i)%len(train)]) for i in range(32)]\n  tc.forward_backward(b,loss_fn='cross_entropy').result()\n  tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result()\nlora=tc.save_weights_and_get_sampling_client(name='rx-bio-judge')\nbase=sc.create_sampling_client(base_model=M)\nsp=tinker.SamplingParams(max_tokens=8,temperature=0.0,stop=r.get_stop_sequences())\nfor name,c in [('base',base),('lora',lora)]:\n  ok=0\n  for msgs,v in evald:\n    out=c.sample(prompt=r.build_generation_prompt(msgs),num_samples=1,sampling_params=sp).result()\n    txt=tok.decode(out.sequences[0].tokens).strip().upper()\n    ok+= (txt.startswith('INCORRECT') and v=='INCORRECT') or (txt.startswith('CORRECT') and v=='CORRECT')\n  print(name,'acc',ok/len(evald))\nEOF",
  "dataset": {
    "hf_path": "qiaojin/PubMedQA",
    "n_examples": 4800,
    "name": "PubMedQA grading pairs (train from pqa_artificial, eval from expert-labeled pqa_labeled)",
    "split": "pqa_artificial/train for training; pqa_labeled/train (500 held-out questions) for eval"
  },
  "eval": {
    "benchmark": "PubMedQA pqa_labeled grading items (expert labels)",
    "metric": "judge verdict accuracy vs expert-derived CORRECT/INCORRECT labels",
    "n_examples": 1000
  },
  "hypothesis": "A Qwen3.5-4B judge LoRA-tuned on 4.8k PubMedQA grading pairs beats the base Qwen3.5-4B, given the same judge prompt, by at least 5 points absolute accuracy on 1,000 grading items built from pqa_labeled.",
  "lora_rank": 32,
  "max_seq_len": 2048,
  "method": "Build grading items from PubMedQA. Each item is (abstract context, question, candidate answer in yes/no/maybe plus a one-line rationale), and the target verdict is CORRECT or INCORRECT. For each question, pair the gold final_decision (CORRECT) with one randomly chosen wrong label (INCORRECT), which keeps the classes balanced. Training items come from pqa_artificial train, whose labels are heuristic. Eval items come from the 1,000 expert-labeled pqa_labeled questions: take 500 questions and make 2 items each, giving 1,000 items with no overlap with training. The judge prompt and Qwen3.5 chat renderer are identical for both arms. The treatment trains a rank-32 LoRA with cross-entropy on the verdict tokens only, using AdamW for 150 steps at batch 32. Both arms are then evaluated by greedy sampling a one-word verdict at temperature 0 and parsing CORRECT/INCORRECT. Unparseable outputs count as wrong. Report accuracy, per-class accuracy, and the rate of unparseable outputs. The only difference between arms is the LoRA SFT.",
  "optimizer": {
    "lr": 0.0001,
    "name": "adam"
  },
  "steps": 150,
  "success_criterion": "Validated if LoRA judge accuracy is at least 5.0 points above base on the 1,000 items. The 95% CI is about \u00b13 points at n=1000, so the gap must also clear 3 points on a paired bootstrap. Failed if the gap is under 3 points or the LoRA's unparseable rate is above 2%.",
  "title": "LoRA SFT judge vs base Qwen3.5-4B on PubMedQA expert-label grading",
  "training_type": "sft"
}
version 2 · by triage-agent · 2d ago · exp_1e2cb2c552
LoRA SFT judge vs base Qwen3.5-4B on PubMedQA answer grading
spec
{
  "base_model": "Qwen/Qwen3.5-4B",
  "baseline": "Base Qwen3.5-4B with no LoRA, using the same judge prompt, renderer, greedy decoding and the same 1000 eval pairs.",
  "batch_size": 32,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 90
  },
  "command": "python judge_sft.py  # custom: PubMedQA->grading-pair dataset builder; training otherwise stock tinker SDK: sc=tinker.ServiceClient(base_url=os.environ['TINKER_BASE_URL']); tc=sc.create_lora_training_client(base_model='Qwen/Qwen3.5-4B', rank=32); for step in range(150): tc.forward_backward(batch_datums(step, bs=32), loss_fn='cross_entropy').result(); tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result(); path=tc.save_weights_for_sampler(name='bio-judge').result().path; eval: sample greedy verdicts from sc.create_sampling_client(model_path=path) and from sc.create_sampling_client(base_model='Qwen/Qwen3.5-4B') on the 1000 pqa_labeled pairs",
  "dataset": {
    "hf_path": "qiaojin/PubMedQA",
    "n_examples": 4800,
    "name": "PubMedQA (pqa_artificial for training, pqa_labeled for expert-label eval)",
    "split": "pqa_artificial/train (train), pqa_labeled/train (eval)"
  },
  "eval": {
    "benchmark": "PubMedQA pqa_labeled grading pairs (balanced correct/flipped candidates, fixed seed)",
    "metric": "judge grading accuracy (CORRECT/INCORRECT exact match vs construction label)",
    "n_examples": 1000
  },
  "hypothesis": "A Qwen3.5-4B LoRA judge trained on PubMedQA grading pairs beats the base Qwen3.5-4B, prompted the same way, by at least 5 points of grading accuracy on 1000 held-out expert-labeled PubMedQA items.",
  "lora_rank": 32,
  "max_seq_len": 1024,
  "method": "Turn PubMedQA into a grading task. Each item gives the judge a question, the abstract context and a candidate answer (a yes/no/maybe verdict plus the long answer). The judge must output CORRECT or INCORRECT. For training, take pqa_artificial train items. Half get their true label as the candidate (target CORRECT) and half get a flipped label (target INCORRECT). For eval, apply the same balanced construction with a fixed seed to all 1000 expert-labeled pqa_labeled items, where flipped candidates draw the wrong verdict from {yes, no, maybe}. Train a rank-32 LoRA on Qwen3.5-4B with cross-entropy on the verdict token only, using AdamW at 1e-4 for 150 steps of batch 32. Both arms use the same renderer, prompt and greedy decoding at eval. The only difference is the LoRA weights.",
  "optimizer": {
    "lr": 0.0001,
    "name": "adam"
  },
  "steps": 150,
  "success_criterion": "Validated if the LoRA judge's grading accuracy is at least 5 points above the base model's on the 1000 pairs (binomial SE is about 1.5 pts). Failed if the gap is under 2 points. In between counts as inconclusive, and the next step is to rerun with 2 more seeds.",
  "title": "LoRA SFT judge vs base Qwen3.5-4B on PubMedQA answer grading",
  "training_type": "sft"
}
version 1 · by triage-agent · 2d ago · exp_f7d448d306
LoRA SFT judge vs zero-shot base judge on PubMedQA answer grading
spec
{
  "base_model": "Qwen/Qwen3.5-4B",
  "baseline": "Base Qwen3.5-4B with no LoRA, prompted zero-shot with the identical grading prompt, renderer, decoding and 1000 eval pairs.",
  "batch_size": 64,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 90
  },
  "command": "python - <<'EOF'\n# Custom part: PubMedQA -> grading-pair data builder; training loop is stock tinker SDK\nimport os, random, tinker\nfrom datasets import load_dataset\nfrom tinker_cookbook import renderers\nfrom tinker_cookbook.tokenizer_utils import get_tokenizer\nfrom tinker_cookbook.supervised.data import conversation_to_datum\nM='Qwen/Qwen3.5-4B'\nrenderer=renderers.get_renderer('qwen3', get_tokenizer(M))\ndef pair(ex):\n    gold=ex['final_decision']; ok=random.random()<0.5\n    cand=gold if ok else random.choice([l for l in ['yes','no','maybe'] if l!=gold])\n    ctx=' '.join(ex['context']['contexts'])\n    return [{'role':'user','content':f'Context: {ctx}\\nQuestion: {ex[\"question\"]}\\nCandidate answer: {cand}\\nIs the candidate answer correct? Reply correct or incorrect.'},\n            {'role':'assistant','content':'correct' if ok else 'incorrect'}]\ntrain=load_dataset('qiaojin/PubMedQA','pqa_artificial',split='train').shuffle(seed=0).select(range(9600))\nsc=tinker.ServiceClient(base_url=os.environ['TINKER_BASE_URL'])\ntc=sc.create_lora_training_client(base_model=M, rank=32)\nfor step in range(150):\n    batch=[conversation_to_datum(pair(train[step*64+i]), renderer, 1024) for i in range(64)]\n    tc.forward_backward(batch, loss_fn='cross_entropy').result()\n    tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result()\nprint(tc.save_weights_for_sampler(name='pubmedqa-judge').result().path)\nEOF\n# Eval: same grading prompt on 1000 pairs from load_dataset('qiaojin/PubMedQA','pqa_labeled'), sampled greedily from the saved LoRA and from the base model",
  "dataset": {
    "hf_path": "qiaojin/PubMedQA",
    "n_examples": 9600,
    "name": "PubMedQA (pqa_artificial for train, pqa_labeled for eval)",
    "split": "pqa_artificial/train"
  },
  "eval": {
    "benchmark": "PubMedQA pqa_labeled grading pairs (balanced correct/incorrect)",
    "metric": "judge accuracy vs gold verdict",
    "n_examples": 1000
  },
  "hypothesis": "A Qwen3.5-4B LoRA judge fine-tuned on PubMedQA pqa_artificial grades candidate answers on expert-labeled pqa_labeled at least 10 points more accurately than the same base model used zero-shot.",
  "lora_rank": 32,
  "max_seq_len": 1024,
  "method": "Turn PubMedQA into a grading task. Each example is (question, abstract context, candidate answer yes/no/maybe), and the target output is 'correct' or 'incorrect'. Build the pairs from gold labels, half with the gold answer and half with a randomly chosen wrong label. The treatment arm trains a rank-32 LoRA on about 9.6k such pairs from pqa_artificial (150 steps x batch 64) with the standard SFT cross-entropy, computed on the assistant turn only. Both arms are evaluated on 1000 balanced grading pairs built from the expert-labeled pqa_labeled split, using the same prompt and renderer. Greedy decoding goes through a sampling client, and the first output token is parsed as the verdict. This is the minimal check that Spindle can produce a usable bio grader before moving to Latch Bio's private data.",
  "optimizer": {
    "lr": 0.0001,
    "name": "adam"
  },
  "steps": 150,
  "success_criterion": "Validated if LoRA judge accuracy minus base judge accuracy is at least 10 points. The standard error is about 1.5 points per arm at n=1000, so gaps under 3 points count as noise. Failed if the gap is under 3 points.",
  "title": "LoRA SFT judge vs zero-shot base judge on PubMedQA answer grading",
  "training_type": "sft"
}

Triage revised the spec 4 times; runs record which version they used.

Runs

statussummarytookbackendstarted bystarted
Succeeded weak evidence for Rank-32 LoRA SFT of Qwen/Qwen3.5-4B (150 steps, bs 32, lr 1e-4, stock cross_entropy/AdamW on 4800 pqa_artificial grading items) raised balanced pqa_labeled grading accuracy from 0.7185 (untuned) to 0.8335 on the same 2000 items under one frozen evaluator.
spec version 5
1h 13m Devin ↗ — 1d ago
Error no result Could not start the Devin session: Devin API error 400: {"detail":"Prompt is too long.
spec version 5
0s Devin — 1d ago
Succeeded no result Train loss 0.803 -> 0.018 over 150 steps on 9600 examples of qiaojin/PubMedQA (Qwen/Qwen3.5-4B, LoRA rank 32, adam lr 0.0001).
spec version 1
19m 21s runner — 2d ago
Source

slack #proj-train-spindle by kailash ↗ · submitted by slack-scraper · 2d ago

raw text
Arjun Banerjee from latch bio has expressed interest in a joint blogpost similar to https://fireworks.ai/customers/heidi-health-ambient-ai-scribe-fine-tuning where he will post-train a reward model on spindle to make synthetic evals for bio.
links triage read (1)
Heidi fine-tuned open models on Fireworks with SFT and then DPO/preference-based 'RFT' and reports that the result beats frontier proprietary models on its internal clinical-note side-by-side eval, while cutting latency from 25s to 7s. The page's stated lessons are to filter preference data aggressively with synthetic rewrites and LLM judges, and to use very large effective batch sizes (about 1M+ tokens) for DPO.
brief

method: Heidi trained open models in two stages. First came SFT, described as imitating foundation-model behavior to learn content style. Then came preference-based fine-tuning, called both DPO and 'Reinforcement Fine-Tuning (RFT)' on the page, which uses preference signals. The preference data came from raw side-by-side (SBS) evaluator choices, which the page says are noisy because evaluators sometimes pick responses that still contain hallucinations. It was cleaned by (a) using frontier models to generate synthetic rewrites as 'golden standard' targets and (b) using an LLM-as-a-Judge to pre-evaluate datasets against safeguard metrics such as Template Adherence and Details Omission. Effective batch size was scaled to roughly 1.5 million tokens using gradient accumulation (Fireworks supports up to 24 steps). Not stated: the base model(s), model sizes, LoRA vs full fine-tuning, learning rate, beta, epochs, dataset size, the judge model, the filtering thresholds, and how the DPO pairs were built from the rewrites and SBS signals. The page is a customer case study, not a paper.

evidence: All results are from Heidi's internal side-by-side evaluation on its clinical note dataset, which is not public. Headline claims: latency fell from 25 seconds to 7 seconds (Figure 1, labelled '3.5X Lower Latency'); POC to production took 4 weeks; the SFT model outperformed 'the Gemini Flash tier of models'; the RFT/DPO model's outputs were preferred over 'the Gemini Pro tier (frontier) models'. The only quantitative quality ablation is that raising the effective batch size from a standard 64k to 768k tokens improved win rate over proprietary models from 48.0% to 51.3%. Not stated: the number of eval examples, the raters (human or LLM), confidence intervals, the exact proprietary models compared, and any ablation of the filtering steps. The page also says the data-filtering strategy was 'confirmed' by experiments but gives no numbers for it. It claims savings ('huge cost savings') without figures.

reproduction: Nothing on the page can be reproduced directly: the data, base model, eval and hyperparameters are all unstated. The page's methodology could be tested on an open task. The linked idea proposes a related but different experiment: LoRA SFT of Qwen3.5-4B as a PubMedQA answer grader or reward model for synthetic bio evals, with latch bio. A minimal test would need the following. Data: PubMedQA (labeled and/or artificial splits) with (question, context, candidate answer) items and gold correctness labels, plus candidate answers from some generator model, some correct and some wrong. Training: LoRA SFT of Qwen3.5-4B to output a verdict or score. Compare against the untuned base model used as a judge, and optionally a larger judge as a reference. Eval: held-out PubMedQA grading accuracy, F1, and agreement with gold labels or with a stronger LLM judge, ideally also checking transfer to another bio QA set. Optional DPO arm: build preference pairs from correct vs incorrect answers, with and without LLM-judge filtering, to test the 'curate aggressively' claim. Batch-size arm: compare small vs large effective batch with gradient accumulation. Compute for a 4B LoRA is small (a single GPU, hours). Gaps: Heidi's task is generative note writing judged by SBS preference, while the idea is a classification-style grader, so the latency and frontier-beating results do not carry over. The 64k to 768k token batch sizes are probably scaled differently for LoRA. The Spindle/Tinker batch and gradient-accumulation controls should be checked before a batch-size ablation. Fireworks-specific infrastructure (24-step accumulation, hosted RFT) is not available in Spindle.

context: The page is a vendor customer story used as a template for a joint blog post: the idea is for latch bio (Arjun Banerjee) to post-train a reward model/grader on Spindle to build synthetic evals for bio, in the style of this Heidi x Fireworks case study. It sits alongside the common 'fine-tuned open model beats frontier on a narrow domain task' narrative, and the SFT-then-DPO recipe with LLM-judge filtering. It relates to the idea only loosely: Heidi trains a generator with SFT/DPO, whereas the idea trains a judge/reward model. The Modal training team might care because it is a marketing-ready, domain-expert collaboration that would exercise Spindle's SFT/DPO and LoRA workflow, and it speaks to the data-quality and large-batch lessons.

caveats: This is marketing copy with no paper, code, or data, and the numbers are internal and unauditable. The 48.0% to 51.3% win-rate gain is small, with no sample size or confidence interval given, so it may be within noise. Crossing 50% against 'proprietary models' is not clearly the same as beating frontier models. The headline 'beats frontier' claim rests on a single internal SBS eval. The page mixes up DPO and 'RFT', which are different things, and does not say which was run. The latency gain may owe as much to model size and serving as to fine-tuning. There is no ablation isolating filtering vs batch size vs algorithm. The page never says whether training was LoRA or full-parameter, so the batch-size finding may not transfer to LoRA. Using frontier-model rewrites and LLM judges creates a risk of circularity if the eval uses similar judges. The page is dated JUL 20, 2026, and is not truncated. For the PubMedQA idea specifically, nothing here shows that SFT improves a grader, so that claim would need its own baseline, such as the untuned Qwen3.5-4B and a strong LLM judge. PubMedQA is also small and its labels are noisy and skewed toward 'yes', so a majority-class baseline must be reported.

triage output
{
  "adjacent_experiment": "",
  "reproducibility_reason": "LoRA SFT of a served Qwen3.5-4B on public PubMedQA verdict data, run as a short tinker SDK loop with stock cross-entropy, then logprob-based grading eval of the trained and base models.",
  "reproducible_on_spindle": true,
  "sources": [
    {
      "brief": {
        "caveats": "This is marketing copy with no paper, code, or data, and the numbers are internal and unauditable. The 48.0% to 51.3% win-rate gain is small, with no sample size or confidence interval given, so it may be within noise. Crossing 50% against 'proprietary models' is not clearly the same as beating frontier models. The headline 'beats frontier' claim rests on a single internal SBS eval. The page mixes up DPO and 'RFT', which are different things, and does not say which was run. The latency gain may owe as much to model size and serving as to fine-tuning. There is no ablation isolating filtering vs batch size vs algorithm. The page never says whether training was LoRA or full-parameter, so the batch-size finding may not transfer to LoRA. Using frontier-model rewrites and LLM judges creates a risk of circularity if the eval uses similar judges. The page is dated JUL 20, 2026, and is not truncated. For the PubMedQA idea specifically, nothing here shows that SFT improves a grader, so that claim would need its own baseline, such as the untuned Qwen3.5-4B and a strong LLM judge. PubMedQA is also small and its labels are noisy and skewed toward 'yes', so a majority-class baseline must be reported.",
        "claim": "Heidi fine-tuned open models on Fireworks with SFT and then DPO/preference-based 'RFT' and reports that the result beats frontier proprietary models on its internal clinical-note side-by-side eval, while cutting latency from 25s to 7s. The page's stated lessons are to filter preference data aggressively with synthetic rewrites and LLM judges, and to use very large effective batch sizes (about 1M+ tokens) for DPO.",
        "context": "The page is a vendor customer story used as a template for a joint blog post: the idea is for latch bio (Arjun Banerjee) to post-train a reward model/grader on Spindle to build synthetic evals for bio, in the style of this Heidi x Fireworks case study. It sits alongside the common 'fine-tuned open model beats frontier on a narrow domain task' narrative, and the SFT-then-DPO recipe with LLM-judge filtering. It relates to the idea only loosely: Heidi trains a generator with SFT/DPO, whereas the idea trains a judge/reward model. The Modal training team might care because it is a marketing-ready, domain-expert collaboration that would exercise Spindle's SFT/DPO and LoRA workflow, and it speaks to the data-quality and large-batch lessons.",
        "evidence": "All results are from Heidi's internal side-by-side evaluation on its clinical note dataset, which is not public. Headline claims: latency fell from 25 seconds to 7 seconds (Figure 1, labelled '3.5X Lower Latency'); POC to production took 4 weeks; the SFT model outperformed 'the Gemini Flash tier of models'; the RFT/DPO model's outputs were preferred over 'the Gemini Pro tier (frontier) models'. The only quantitative quality ablation is that raising the effective batch size from a standard 64k to 768k tokens improved win rate over proprietary models from 48.0% to 51.3%. Not stated: the number of eval examples, the raters (human or LLM), confidence intervals, the exact proprietary models compared, and any ablation of the filtering steps. The page also says the data-filtering strategy was 'confirmed' by experiments but gives no numbers for it. It claims savings ('huge cost savings') without figures.",
        "method": "Heidi trained open models in two stages. First came SFT, described as imitating foundation-model behavior to learn content style. Then came preference-based fine-tuning, called both DPO and 'Reinforcement Fine-Tuning (RFT)' on the page, which uses preference signals. The preference data came from raw side-by-side (SBS) evaluator choices, which the page says are noisy because evaluators sometimes pick responses that still contain hallucinations. It was cleaned by (a) using frontier models to generate synthetic rewrites as 'golden standard' targets and (b) using an LLM-as-a-Judge to pre-evaluate datasets against safeguard metrics such as Template Adherence and Details Omission. Effective batch size was scaled to roughly 1.5 million tokens using gradient accumulation (Fireworks supports up to 24 steps). Not stated: the base model(s), model sizes, LoRA vs full fine-tuning, learning rate, beta, epochs, dataset size, the judge model, the filtering thresholds, and how the DPO pairs were built from the rewrites and SBS signals. The page is a customer case study, not a paper.",
        "reproduction": "Nothing on the page can be reproduced directly: the data, base model, eval and hyperparameters are all unstated. The page's methodology could be tested on an open task. The linked idea proposes a related but different experiment: LoRA SFT of Qwen3.5-4B as a PubMedQA answer grader or reward model for synthetic bio evals, with latch bio. A minimal test would need the following. Data: PubMedQA (labeled and/or artificial splits) with (question, context, candidate answer) items and gold correctness labels, plus candidate answers from some generator model, some correct and some wrong. Training: LoRA SFT of Qwen3.5-4B to output a verdict or score. Compare against the untuned base model used as a judge, and optionally a larger judge as a reference. Eval: held-out PubMedQA grading accuracy, F1, and agreement with gold labels or with a stronger LLM judge, ideally also checking transfer to another bio QA set. Optional DPO arm: build preference pairs from correct vs incorrect answers, with and without LLM-judge filtering, to test the 'curate aggressively' claim. Batch-size arm: compare small vs large effective batch with gradient accumulation. Compute for a 4B LoRA is small (a single GPU, hours). Gaps: Heidi's task is generative note writing judged by SBS preference, while the idea is a classification-style grader, so the latency and frontier-beating results do not carry over. The 64k to 768k token batch sizes are probably scaled differently for LoRA. The Spindle/Tinker batch and gradient-accumulation controls should be checked before a batch-size ablation. Fireworks-specific infrastructure (24-step accumulation, hosted RFT) is not available in Spindle.",
        "title": "Heidi x Fireworks: Bridging the gap in frontier model performance"
      },
      "error": null,
      "full_text": true,
      "kind": "page",
      "title": "Heidi x Fireworks: Bridging the gap in frontier model performance",
      "url": "https://fireworks.ai/customers/heidi-health-ambient-ai-scribe-fine-tuning"
    }
  ],
  "spec": {
    "activation_gate": {
      "applicable": true,
      "checks": [
        {
          "measures": "model name reported by the created training/sampling client for both arms",
          "metric": "executed_base_model",
          "one_of": [
            "Qwen/Qwen3.5-4B"
          ]
        },
        {
          "max": 0.51,
          "measures": "fraction of the 2000 eval items whose gold verdict is CORRECT",
          "metric": "eval_correct_label_fraction",
          "min": 0.49
        },
        {
          "max": 0,
          "measures": "count of eval PMIDs present in the training items",
          "metric": "train_eval_pmid_overlap",
          "min": 0
        },
        {
          "measures": "relative drop in mean verdict-token train NLL from step 0 to step 20 of the treatment pilot",
          "metric": "verdict_token_loss_drop_pilot",
          "min": 0.1
        },
        {
          "max": 0.95,
          "measures": "fraction of the first 200 eval items the base model labels CORRECT",
          "metric": "base_predicted_correct_fraction",
          "min": 0.05
        }
      ],
      "reason": "Several things must hold before the comparison means anything. The executed model and arms must match the spec. The eval must be balanced and disjoint from training. SFT must actually move the verdict-token loss. The base judge must not be degenerate: if it always answers one class, its 50% is trivial and has to be flagged."
    },
    "arms": [
      {
        "name": "base_untuned",
        "seed": 0,
        "trained": false
      },
      {
        "lr": 0.0001,
        "name": "lora_sft_grader",
        "seed": 0,
        "steps": 150,
        "trained": true
      }
    ],
    "base_model": "Qwen/Qwen3.5-4B",
    "baseline": "Untuned Qwen/Qwen3.5-4B with the same prompt, renderer, eval items and logprob scoring.",
    "batch_size": 32,
    "budget": {
      "gpu_hours": 1.5,
      "max_wall_minutes": 120
    },
    "command": "python grader_sft.py  # tinker SDK; nothing custom except dataset construction and logprob eval\nimport os, random, tinker; from datasets import load_dataset\nfrom tinker_cookbook import renderers; from tinker_cookbook.tokenizer_utils import get_tokenizer\nM='Qwen/Qwen3.5-4B'; svc=tinker.ServiceClient(base_url=os.environ['TINKER_BASE_URL'])\ntok=get_tokenizer(M); r=renderers.get_renderer('qwen3', tok)\ndef items(ds, n, rng):\n  out=[]\n  for ex in list(ds)[:n]:\n    g=ex['final_decision']; w=rng.choice([o for o in ['yes','no','maybe'] if o!=g])\n    for c,lab in [(g,'CORRECT'),(w,'INCORRECT')]:\n      p=f\"Context: {' '.join(ex['context']['contexts'])}\\nQuestion: {ex['question']}\\nCandidate answer: {c}\\nIs the candidate answer correct? Reply CORRECT or INCORRECT.\"\n      out.append((ex['pubid'],p,lab))\n  return out\nrng=random.Random(0)\ntrain=items(load_dataset('qiaojin/PubMedQA','pqa_artificial',split='train').shuffle(seed=0),2400,rng)\nev=items(load_dataset('qiaojin/PubMedQA','pqa_labeled',split='train'),1000,rng)\nassert not {a for a,_,_ in train}&{a for a,_,_ in ev}\nrandom.Random(0).shuffle(train)\ndef datum(p,lab):  # weights only on verdict tokens\n  return renderers.build_supervised_example_datum(r,[{'role':'user','content':p},{'role':'assistant','content':lab}],max_length=1024)  # last-assistant-only weights\ntc=svc.create_lora_training_client(base_model=M, rank=32)\nfor s in range(150):\n  b=[datum(p,l) for _,p,l in train[s*32:(s+1)*32]]\n  tc.forward_backward(b,'cross_entropy').result(); tc.optim_step(tinker.AdamParams(learning_rate=1e-4)).result()\nsc_t=tc.save_weights_and_get_sampling_client('pubmedqa-grader'); sc_b=svc.create_sampling_client(base_model=M)\ndef verdict_lp(sc,p,lab):  # summed logprob of the verdict tokens\n  pre=r.build_generation_prompt([{'role':'user','content':p}]); comp=tok.encode(lab,add_special_tokens=False)\n  mi=tinker.ModelInput.from_ints(pre.to_ints()+comp); lp=sc.compute_logprobs(mi).result(); return sum(lp[-len(comp):])\nfor name,sc in [('baseline',sc_b),('treatment',sc_t)]:\n  acc=[int((verdict_lp(sc,p,'CORRECT')>verdict_lp(sc,p,'INCORRECT'))==(l=='CORRECT')) for _,p,l in ev]\n  print(name, sum(acc)/len(acc))  # save per-item 0/1 for the paired Hoeffding rule",
    "dataset": {
      "hf_path": "qiaojin/PubMedQA",
      "n_examples": 4800,
      "name": "PubMedQA (pqa_artificial train, pqa_labeled eval)",
      "split": "pqa_artificial:train (2400 questions -> 4800 grading items); eval pqa_labeled:train (1000 questions -> 2000 items)"
    },
    "decision_rule": {
      "alpha": 0.05,
      "criteria": [
        {
          "bounds": [
            0,
            1
          ],
          "id": "primary",
          "margin": 0.03,
          "metric": "accuracy",
          "weights": {
            "baseline": -1,
            "treatment": 1
          }
        }
      ],
      "estimand": "Per-item grading accuracy on balanced PubMedQA pqa_labeled grading items: LoRA-SFT Qwen3.5-4B minus untuned Qwen3.5-4B, higher is better; inference conditional on the seed-0 checkpoint",
      "method": "paired_hoeffding",
      "seeds": [
        0
      ],
      "unit": "fixed_checkpoint_items",
      "version": 1
    },
    "eval": {
      "aggregation": null,
      "benchmark": "PubMedQA pqa_labeled balanced grading set (1 correct + 1 incorrect candidate per question)",
      "metric": "grading accuracy (argmax logprob of CORRECT vs INCORRECT vs gold verdict)",
      "n_examples": 2000
    },
    "experiment_features": {
      "adaptive_selection": false,
      "checkpoint_restore": false,
      "custom_loss": false,
      "custom_optimizer": false,
      "generated_training_data": false,
      "heldout_loss": false,
      "learned_generator": false,
      "paired_comparison": true,
      "paired_training_arms": false,
      "warmup_adaptation": false
    },
    "feasibility": {
      "config": {
        "arms": [
          "base_untuned",
          "lora_sft_grader"
        ],
        "base_model": "Qwen/Qwen3.5-4B",
        "batch_size": 32,
        "max_seq_len": 1024,
        "n_eval": 200,
        "steps": 20
      },
      "effective_config": {
        "arms": [
          "base_untuned",
          "lora_sft_grader"
        ],
        "base_model": "Qwen/Qwen3.5-4B",
        "batch_size": 32,
        "max_seq_len": 1024,
        "n_eval": 2000,
        "steps": 150
      },
      "generation": null,
      "measurements": null,
      "primary_endpoint": "pubmedqa_labeled_grading_accuracy",
      "recovery": null,
      "source": null
    },
    "feasibility_check": {
      "generation_applicability": {
        "check": "generation_audit",
        "enforce": false,
        "features": {
          "generated_training_data": false
        },
        "reasons": [
          "spec.generation_audit opts out and no feature it covers is true"
        ],
        "status": "not_applicable"
      },
      "generation_counts": null,
      "gpu_hours": null,
      "known_gpu_hours": 0,
      "phases": {},
      "problems": [
        "Re-projection required: lora_rank changed or was not recorded",
        "Re-projection required: dataset changed or was not recorded",
        "Re-projection required: steps changed or was not recorded",
        "Re-projection required: n_examples changed or was not recorded",
        "Re-projection required: n_eval changed or was not recorded",
        "Pilot evidence source not recorded",
        "Representative setup timing not recorded",
        "Representative training timing not recorded",
        "Representative evaluation timing not recorded",
        "Positive recovery reserve not recorded"
      ],
      "status": "unknown",
      "wall_seconds": null
    },
    "feasibility_required": true,
    "generation_audit": {
      "answer_comparison": "categorical_exact",
      "max_eval_overlaps": 0,
      "max_label_errors": 0,
      "min_sample_size": 100,
      "required": false
    },
    "hypothesis": "LoRA SFT on 4,800 PubMedQA-artificial grading items raises Qwen3.5-4B grading accuracy on balanced expert-labeled PubMedQA grading items by more than 3 points over the untuned base model under the same prompt and scoring, with the lower confidence bound above that margin.",
    "lora_rank": 32,
    "max_seq_len": 1024,
    "method": "Grading items are built deterministically from PubMedQA, with no model-generated data. Each item is (question, abstract contexts, candidate decision in {yes, no, maybe}) with target CORRECT if the candidate equals final_decision, else INCORRECT. Each question gets one correct candidate and one wrong candidate, drawn with seed 0 from the remaining options, so the classes are 50/50 by construction. Training data is 2,400 questions from pqa_artificial (whose labels are auto-derived yes/no), giving 4,800 items. The eval set is all 1,000 expert-labeled pqa_labeled questions, giving 2,000 items. pqa_artificial and pqa_labeled are disjoint by PMID, and this is checked. The treatment is LoRA rank 32 on Qwen/Qwen3.5-4B, trained with a tinker SDK forward_backward + optim_step loop using the stock cross_entropy loss. Loss weight sits only on the verdict tokens. AdamW at lr 1e-4, batch 32, 150 steps (about 1 epoch). The baseline is the untuned Qwen3.5-4B with the identical renderer and prompt. Both are scored by logprob: the predicted verdict is whichever of the 'CORRECT' and 'INCORRECT' completions has the higher summed logprob, computed via forward/compute_logprobs, so there is no sampling noise and no parse failures. The majority-class rate (50%) is reported, along with accuracy on the subsets where the gold label is 'maybe'. Nothing is custom server-side and there is no Spindle fork. correctness_preflight: {\"required\": false, \"reason\": \"stock tinker cross_entropy loss and AdamW; no custom loss or optimizer implementation\"}. Heidi's DPO, batch-size and filtering claims are out of scope for this first node.",
    "optimizer": {
      "lr": 0.0001,
      "name": "adam"
    },
    "primary_endpoint": "pubmedqa_labeled_grading_accuracy",
    "steps": 150,
    "success_criterion": "Treatment minus baseline grading accuracy on the 2000 paired eval items exceeds 0.03, with the paired Hoeffding 95% lower bound above 0.03 (half-width about 0.06 at n=2000, so an observed gain of roughly 0.09 or more is needed). Otherwise the result is not validated, or failed if the point estimate is <= 0.",
    "title": "LoRA-SFT PubMedQA grader vs untuned Qwen3.5-4B judge",
    "training_type": "sft"
  },
  "summary": "Qwen3.5-4B is LoRA-tuned to label PubMedQA candidate answers CORRECT or INCORRECT; we compare its grading accuracy on 2000 expert-labeled items with the untuned base.",
  "tags": [
    "sft",
    "reward-model",
    "eval",
    "bio",
    "lora",
    "data"
  ],
  "title": "Does LoRA SFT make Qwen3.5-4B a better PubMedQA grader?",
  "value_estimate": 3,
  "value_rationale": "This seeds a customer-facing joint blog post with latch bio and exercises Spindle's SFT and logprob eval paths. The Heidi page is unauditable marketing, though, and a 4B grader on PubMedQA verdicts is a narrow proxy for the synthetic bio evals they actually want."
}

Comments

anonymous · 2d ago
Started a session.
@pre-mortem · 2d ago · 42/100 · Add a sanity step first: base-model accuracy on plain PubMedQA, parse-failure rate, and a train-on-s…
42/100 · Add a sanity step first: base-model accuracy on plain PubMedQA, parse-failure rate, and a train-on-shuffled-labels control. Also say what a base score near 90% means. Otherwise a flat result won't distinguish a task shortcut, ceiling, or pipeline bug from SFT genuinely not helping. 2/5 riskiest-assumption: Unstated. Key risk is that flipped-verdict grading just reduces to PubMedQA yes/no/maybe answering, so SFT gains may not reflect grading. Plan doesn't target it first. 3/5 negative-result-value: Under 2 points narrows to 'SFT doesn't help here', but base may already be near ceiling or the task artificial; no diagnostic separates these. 2/5 setup-sanity-checks: Same prompt and seed for both arms, but no known reference number, e.g. base PubMedQA accuracy, no format-parse-rate check, no majority-class or shuffled-label control. 4/5 kill-criteria: Explicit: under 2 points is failure, 5+ is validated, in-between triggers two more seeds. Missing what ends the line afterward, and no ceiling-effect or Qwen3.5 thinking-mode exclusion. Pre-mortem rubric v1
@prior-art · 2d ago · 28/100 · Before running, ask in channel whether anyone has trained judge SFT on Spindle, and sync with Arjun'…
28/100 · Before running, ask in channel whether anyone has trained judge SFT on Spindle, and sync with Arjun's own reward-model work. Cite JudgeLM and Prometheus, and say what this adds. As built, you measure PubMedQA QA skill, not grading, so redesign the eval first. 2/5 novelty: SFT of an LLM judge on in-domain labeled pairs versus its base is a well-established recipe; PubMedQA is a new setting but the answer is fairly predictable. 1/5 positioning: No related work named: no mention of JudgeLM, Prometheus, PandaLM, or reward-bench-style judge fine-tuning, nor of any prior PubMedQA fine-tuning results. The delta is unstated. 3/5 duplication-risk: Judge or reward-model SFT is a common Spindle demo, and Latch Bio's Arjun is separately post-training a reward model. Nothing says whether Modal has already run this or coordinates with him. 2/5 insight: Only a vague expectation that in-domain training helps. The task is constructed as yes/no/maybe label matching, so the judge may just learn PubMedQA answering, and no mechanism is argued. Prior Art rubric v1
@compute-hawk · 2d ago · 72/100 · Cheap and well within budget, so run it with a second seed and a few-shot base baseline. Cap max_tok…
72/100 · Cheap and well within budget, so run it with a second seed and a few-shot base baseline. Cap max_tokens or disable thinking for the base arm, or sampling dominates. Also add a harder, non-flip eval, since this task may be trivial and not reflect Latch's judging. 5/5 budget-fit: 150 steps × 32 × ~600 tokens ≈ 3M training tokens on 4B LoRA, plus 2×1000 short greedy single-token evals. Likely well under 1 GPU-hour; room for extra seeds. 3/5 signal-per-gpu-hour: Compute goes to the right comparison, but the task is easy and mostly label-flip detection, so a 1.5B proxy or a few-shot-prompted baseline might answer cheaper. 3/5 small-scale-validity: 4B is already the target scale, but a gain on synthetic flip grading may not transfer to Latch's real bio answer grading; needs a bigger or realistic-data confirmation. 4/5 throughput-risk: Sequences ≤1024 and verdict-only outputs make wall clock predictable. Risk: if Qwen3.5 base emits thinking text, greedy base eval could sample long unless capped. Compute Hawk rubric v1
@reproducibility · 2d ago · 56/100 · Data is well named, but the evidence is a customer's interest, not a result. Commit the dataset buil…
56/100 · Data is well named, but the evidence is a customer's interest, not a result. Commit the dataset builder with the seed value, pin dataset and model revisions, and note that grading synthetic flipped labels isn't Latch's bio-answer grading, so the claim may not transfer. 5/5 data-available: qiaojin/PubMedQA with pqa_artificial/train for training and pqa_labeled/train for eval is named precisely; the 1000 eval items are exact. Config names need checking. 3/5 stock-tooling: Training loop is stock Tinker SDK with cross_entropy, but a custom judge_sft.py dataset builder and verdict-token-only loss masking are needed. Qwen3.5-4B support is assumed. 3/5 rerunnable: Command, lr, rank, steps and batch size are recorded, and the eval uses a fixed seed. But the seed value, model revision and dataset revision are unpinned, and the training sampling seed is unspecified. 1/5 claim-evidence: The only source is a message that Latch Bio is interested in a blogpost, and the Fireworks link is a marketing page. No numbers show a tuned judge beating base. Reproducibility Auditor rubric v1
@scope-cutter · 2d ago · 69/100 · Cleanly one-variable, which I like. Before 150 steps, run a 30-step pass on a smaller model and chec…
69/100 · Cleanly one-variable, which I like. Before 150 steps, run a 30-step pass on a smaller model and check the base model's accuracy first; if base is near ceiling, stop. Then spell out the ladder: seeds, harder negatives, bigger model. 5/5 one-variable: Same Qwen3.5-4B, prompt, renderer, greedy decoding and 1000 eval pairs; only LoRA weights differ. Clean attribution. 3/5 smallest-version: Already modest (4B, 150 steps, 90 min), but a 1.7B model, ~1k steps-worth of data or 30 steps could show a signal first. 3/5 incremental-path: Inconclusive case says rerun 2 seeds, but no ladder after a win: no larger model, harder negatives, or non-PubMedQA transfer toward Latch's use. Scope Cutter rubric v1
@spindle-customer · 2d ago · 53/100 · Latch will want a judge that grades free-form answers, not one that detects flipped yes/no labels th…
53/100 · Latch will want a judge that grades free-form answers, not one that detects flipped yes/no labels the base model may already ace. Add a base-model accuracy check first, and try a harder grading task, or the headline gap may be trivial. 3/5 customer-action: Latch Bio's reward-model question is real, but PubMedQA yes/no flip grading is a weak proxy for grading free-form bio answers, so it only informs the decision. 4/5 recipe-potential: Stock tinker SFT loop plus a pair builder gives a copyable before/after for training a domain judge; the dataset builder needs generalizing. 2/5 product-feedback: Plain cross-entropy SFT with AdamW on a 4B model is a well-trodden path; the only novelty is Qwen3.5-4B and sampling from a saved checkpoint. 4/5 story: Two sentences and a number: LoRA judge vs base accuracy on 1000 expert-labeled items. Caveat: labels come from construction, not grader preference. Spindle Customer Lens rubric v1
@reviewer-2 · 2d ago · 50/100 · The 5-point threshold is good, but a tuned judge beating an untuned base proves little. Add a few-sh…
50/100 · The 5-point threshold is good, but a tuned judge beating an untuned base proves little. Add a few-shot or prompt-tuned base, check parse failures, and run at least 2 training seeds up front. Your eval also tests label verification, not bio answer grading, so the Latch claim is overreach. 5/5 falsifiable-hypothesis: LoRA judge beats base by ≥5 points on 1000 items; fails under 2 points, with inconclusive band predefined. Thresholds set before the run. 2/5 fair-baseline: Base is untuned zero-shot, so it favors the treatment. No few-shot or prompt-tuned base, and no format-only SFT control to separate task learning from format learning. 3/5 confounds: Prompt, renderer, and decoding are shared, but the base may fail on output format. Single lr, no sweep. Qwen3.5 thinking mode is unspecified, and the pqa_artificial vs pqa_labeled shift is uncontrolled. 3/5 noise: 1000 items gives ~1.5pt SE, which resolves a 5pt gap. But training is single-seed, and seeds are only added after an inconclusive result. 2/5 eval-validity: Task is PubMedQA yes/no/maybe verification with synthetic flipped labels, not grading free-form bio answers. Latch's reward-model claim isn't tested, and the balanced flip construction may be gameable. Reviewer 2 rubric v1
anonymous · 2d ago
Started a session.

Submit an idea

Most ideas come in from Slack threads. Use this when yours is not in one yet; the triage agent picks it up.