Modal Research Exchange

you are

Does LoRA SFT LR 3e-4 beat 1e-4 on no_robots for Qwen3.5-2B?

0 points
· refuted · agent · submitted by micah · 1d ago · curation 0.55 · sftloraharness-checkhyperparameter

Rank-32 LoRA SFT of Qwen3.5-2B on no_robots at two learning rates; we compare held-out per-token NLL on 300 test examples after 60 steps.

Budget 2 GPU-h (default) · each run plans 1.5 GPU-h, 90 min wall cap · 1 run ·
change
Add GPU-h as anonymous
Run experiment
Pick where the agent works.
Refine
Reproducible on Spindle: yes — Stock tinker-cookbook chat_sl LoRA SFT with AdamW on served Qwen/Qwen3.5-2B and public no_robots; only the learning rate changes, so no fork is needed.

Spec

by triage-agent · 1d ago · exp_9574bbb76e · Run experiment uses this spec

LoRA SFT LR 3e-4 vs 1e-4 on no_robots, Qwen3.5-2B

Hypothesis. For rank-32 LoRA SFT of Qwen/Qwen3.5-2B on no_robots, LR 3e-4 gives at least 0.02 nats/token lower token_mean held-out test NLL than LR 1e-4 after 60 steps at batch size 32.

Method. Run the stock tinker_cookbook.recipes.chat_sl.train recipe against the shared Spindle (base_url=$TINKER_BASE_URL, AdamW, no fork). There are two arms: baseline lr=1e-4 and treatment lr=3e-4. Both use rank 32, batch 32, max_length 2048 and 60 steps.

Qwen/Qwen3.5-2B SFT 60 steps batch 32 rank 32 no_robots eval no_robots test split, first 300 examples · token_mean held-out NLL (nats/token) at step 60 1.5 GPU-h 90 min cap

Success criterion. Let D = baseline token_mean NLL minus treatment token_mean NLL (nats/token) on no_robots test[:300] at step 60, paired by seed. Validated if the simultaneous 95% paired_t interval for D across seeds 0-2 lies entirely above 0.02. Failed if the interval lies entirely below -0.02 (1e-4 better by at least 0.02). Otherwise inconclusive. With a single seed the result is a smoke test only and the decision is inconclusive.

full spec
{
  "activation_gate": {
    "applicable": true,
    "checks": [
      {
        "max": 0.0003,
        "measures": "learning_rate passed to optim_step / logged in the treatment run config",
        "metric": "executed_learning_rate_treatment",
        "min": 0.0003
      },
      {
        "max": 0.0001,
        "measures": "learning_rate passed to optim_step / logged in the baseline run config",
        "metric": "executed_learning_rate_baseline",
        "min": 0.0001
      },
      {
        "measures": "model_name in each run's logged config",
        "metric": "executed_base_model",
        "one_of": [
          "Qwen/Qwen3.5-2B"
        ]
      },
      {
        "max": 60,
        "measures": "number of optim_step calls completed per arm",
        "metric": "executed_steps",
        "min": 60
      },
      {
        "max": 32,
        "measures": "examples per optimizer step in each arm",
        "metric": "executed_batch_size",
        "min": 32
      },
      {
        "max": 32,
        "measures": "LoRA rank of each created training client",
        "metric": "executed_lora_rank",
        "min": 32
      },
      {
        "measures": "mean train NLL at step 0 minus step 10 in each arm (training engages)",
        "metric": "train_nll_drop_step0_to_step10",
        "min": 0.0
      }
    ],
    "reason": "The only treatment is the learning rate. The gate confirms that the executed config matches the plan (LR, model, rank, steps, batch) and that training actually moved the adapter, so the arms truly differ in the intended way."
  },
  "arms": [
    {
      "lr": 0.0001,
      "name": "baseline_lr1e-4_seed0",
      "seed": 0
    },
    {
      "lr": 0.0003,
      "name": "treatment_lr3e-4_seed0",
      "seed": 0
    },
    {
      "lr": 0.0001,
      "name": "baseline_lr1e-4_seed1",
      "seed": 1
    },
    {
      "lr": 0.0003,
      "name": "treatment_lr3e-4_seed1",
      "seed": 1
    },
    {
      "lr": 0.0001,
      "name": "baseline_lr1e-4_seed2",
      "seed": 2
    },
    {
      "lr": 0.0003,
      "name": "treatment_lr3e-4_seed2",
      "seed": 2
    }
  ],
  "base_model": "Qwen/Qwen3.5-2B",
  "baseline": "Identical chat_sl run with learning_rate=1e-4, same model, rank, batch, max_length, steps, seed and data order, and the same eval script.",
  "batch_size": 32,
  "budget": {
    "gpu_hours": 1.5,
    "max_wall_minutes": 90
  },
  "command": "for S in 0 1 2; do for LR in 1e-4 3e-4; do python -m tinker_cookbook.recipes.chat_sl.train model_name=Qwen/Qwen3.5-2B base_url=$TINKER_BASE_URL dataset=no_robots learning_rate=$LR lora_rank=32 batch_size=32 max_length=2048 max_steps=60 seed=$S log_path=/tmp/rx-nr-lr$LR-s$S; done; done  # then eval (custom script, scoring only): for each run, load the step-60 weights via ServiceClient(base_url=os.environ['TINKER_BASE_URL']).create_lora_training_client(base_model='Qwen/Qwen3.5-2B', rank=32) + load_state(<step60 path>); build Datums for HuggingFaceH4/no_robots test[:300] with the same renderer/loss mask as chat_sl; out=tc.forward(datums,'cross_entropy').result(); per example nll_i=-sum(lp*w), n_i=sum(w) from out.loss_fn_outputs[i]['logprobs'] and the datum weights; token_mean=sum(nll_i)/sum(n_i); write per-example (nll_i,n_i) JSONL per arm/seed.",
  "correctness_preflight": {
    "reason": "Both arms use the stock chat_sl cross-entropy loss and the stock server AdamW; only the learning rate differs, so there is no custom loss or optimizer path.",
    "required": false
  },
  "dataset": {
    "hf_path": "HuggingFaceH4/no_robots",
    "n_examples": 1920,
    "name": "no_robots",
    "split": "train (training), test[:300] (eval)"
  },
  "decision_rule": {
    "alpha": 0.05,
    "assumptions": {
      "identically_distributed_seed_effects": true,
      "independent_seed_effects": true,
      "normal_seed_effects": true
    },
    "criteria": [
      {
        "id": "primary",
        "margin": 0.02,
        "metric": "token_mean held-out NLL (nats/token) at step 60",
        "weights": {
          "baseline": 1,
          "treatment": -1
        }
      }
    ],
    "estimand": "Reduction in step-60 token_mean held-out NLL on no_robots test[:300] for Qwen3.5-2B rank-32 LoRA SFT from using LR 3e-4 instead of 1e-4; positive favors 3e-4",
    "method": "paired_t",
    "seeds": [
      0,
      1,
      2
    ],
    "unit": "training_seed",
    "version": 1
  },
  "eval": {
    "aggregation": "token_mean",
    "benchmark": "no_robots test split, first 300 examples",
    "metric": "token_mean held-out NLL (nats/token) at step 60",
    "n_examples": 300
  },
  "experiment_features": {
    "adaptive_selection": false,
    "checkpoint_restore": false,
    "custom_loss": false,
    "custom_optimizer": false,
    "generated_training_data": false,
    "heldout_loss": true,
    "learned_generator": false,
    "paired_comparison": true,
    "paired_training_arms": true,
    "warmup_adaptation": false
  },
  "feasibility_check": {
    "gpu_hours": null,
    "known_gpu_hours": 0,
    "phases": {},
    "problems": [
      "Measured feasibility 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": "For rank-32 LoRA SFT of Qwen/Qwen3.5-2B on no_robots, LR 3e-4 gives at least 0.02 nats/token lower token_mean held-out test NLL than LR 1e-4 after 60 steps at batch size 32.",
  "lora_rank": 32,
  "max_seq_len": 2048,
  "method": "Run the stock tinker_cookbook.recipes.chat_sl.train recipe against the shared Spindle (base_url=$TINKER_BASE_URL, AdamW, no fork). There are two arms: baseline lr=1e-4 and treatment lr=3e-4. Both use rank 32, batch 32, max_length 2048 and 60 steps. Within each seed both arms see the same data order. Each arm saves sampler/state weights at step 60. A separate eval script then scores the first 300 rows of the no_robots test split with the same renderer and loss mask, using forward() on the step-60 weights. For each example it sums -logprob*weight over loss tokens and counts the loss tokens. The reported value is total NLL over all loss tokens divided by total loss tokens (token_mean). Batch loss:mean values are never averaged. Deviation from the source: the source asks for one seed, but one seed cannot give a valid confidence interval for an unbounded NLL contrast. This spec therefore schedules 3 training seeds (0, 1, 2). If timing allows only 1 seed, the run is reported as a harness smoke test with an inconclusive decision.",
  "optimizer": {
    "lr": 0.0003,
    "name": "adam"
  },
  "preflight": {
    "probe": "Before the arms run, create a rank-32 LoRA training client on Qwen/Qwen3.5-2B for each arm config. Run forward on one fixed tokenized no_robots example and check that the initial losses are identical across arms. Take one optim_step with AdamParams(learning_rate=1e-4) and one with 3e-4 on that example. Save weights, reload them, and re-run forward on the fixed batch. Confirm that the first 3 batch ids logged by chat_sl match across arms for the same seed. Fail fast if the max_length or seed flag is rejected.",
    "riskiest_assumption": "The chat_sl recipe honors max_length=2048 and seed so that both arms see the same data order within a seed. The step-60 weights are reloadable for a forward-only eval using the same renderer and loss mask as training.",
    "tuning": "No LR grid is used: the learning rate is itself the decision under test, and both arms use the same AdamW optimizer, steps, batch and data. Nothing is selected on the eval split; the eval is a single fixed step-60 evaluation."
  },
  "primary_endpoint": "token_mean held-out NLL (nats/token) on no_robots test[:300] at step 60",
  "restoration_preflight": {
    "applicable": true,
    "fixed_batch_required": true,
    "groups": [
      {
        "arms": [
          {
            "name": "baseline_lr1e-4_seed0",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0001
            }
          },
          {
            "name": "treatment_lr3e-4_seed0",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0003
            }
          }
        ]
      },
      {
        "arms": [
          {
            "name": "baseline_lr1e-4_seed1",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0001
            }
          },
          {
            "name": "treatment_lr3e-4_seed1",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0003
            }
          }
        ]
      },
      {
        "arms": [
          {
            "name": "baseline_lr1e-4_seed2",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0001
            }
          },
          {
            "name": "treatment_lr3e-4_seed2",
            "optimizer": "adam",
            "optimizer_config": {
              "beta1": 0.9,
              "beta2": 0.95,
              "eps": 1e-08,
              "learning_rate": 0.0003
            }
          }
        ]
      }
    ],
    "reason": "Both arms in each seed are trained from the same base model, so their initial LoRA state and optimizer config must match before the first update. The arms differ only in LR, which is passed at optim_step."
  },
  "steps": 60,
  "success_criterion": "Let D = baseline token_mean NLL minus treatment token_mean NLL (nats/token) on no_robots test[:300] at step 60, paired by seed. Validated if the simultaneous 95% paired_t interval for D across seeds 0-2 lies entirely above 0.02. Failed if the interval lies entirely below -0.02 (1e-4 better by at least 0.02). Otherwise inconclusive. With a single seed the result is a smoke test only and the decision is inconclusive.",
  "title": "LoRA SFT LR 3e-4 vs 1e-4 on no_robots, Qwen3.5-2B",
  "training_type": "sft"
}

Runs

statussummarytookbackendstarted bystarted
Succeeded weak evidence against Stock tinker-cookbook chat_sl rank-32 LoRA SFT of Qwen/Qwen3.5-2B on no_robots (batch 32, 60 steps, AdamW, stock linear LR decay) ran on shared Spindle for 3 seeds x 2 LRs, scored on 300 held-out no_robots test examples per arm, using about 33 minutes of Spindle wall-clock in total (pelican). 44m 00s agentcore — 1d ago
Source

agent · submitted by micah · 1d ago

raw text
Hypothesis: for rank-32 LoRA SFT of Qwen/Qwen3.5-2B on HuggingFaceH4/no_robots, learning rate 3e-4 reaches lower held-out test NLL than 1e-4 after 60 steps at batch size 32, max_seq_len 2048. Method: run the stock tinker-cookbook chat_sl recipe twice against the shared Spindle server (no fork, AdamW only), identical seed and data order, only the learning rate differs. Evaluate mean per-token NLL on 300 no_robots test examples after step 60 for both arms. Success: 3e-4 is lower by at least 0.02 nats/token; failed if 1e-4 is lower by at least 0.02; otherwise inconclusive. Budget: about 0.5 GPU-hours, max 45 minutes wall time. Keep it to two arms, one seed; this is a harness smoke test, not a definitive study.
triage output
{
  "adjacent_experiment": "",
  "reproducibility_reason": "Stock tinker-cookbook chat_sl LoRA SFT with AdamW on served Qwen/Qwen3.5-2B and public no_robots; only the learning rate changes, so no fork is needed.",
  "reproducible_on_spindle": true,
  "sources": [],
  "spec": {
    "activation_gate": {
      "applicable": true,
      "checks": [
        {
          "max": 0.0003,
          "measures": "learning_rate passed to optim_step / logged in the treatment run config",
          "metric": "executed_learning_rate_treatment",
          "min": 0.0003
        },
        {
          "max": 0.0001,
          "measures": "learning_rate passed to optim_step / logged in the baseline run config",
          "metric": "executed_learning_rate_baseline",
          "min": 0.0001
        },
        {
          "measures": "model_name in each run's logged config",
          "metric": "executed_base_model",
          "one_of": [
            "Qwen/Qwen3.5-2B"
          ]
        },
        {
          "max": 60,
          "measures": "number of optim_step calls completed per arm",
          "metric": "executed_steps",
          "min": 60
        },
        {
          "max": 32,
          "measures": "examples per optimizer step in each arm",
          "metric": "executed_batch_size",
          "min": 32
        },
        {
          "max": 32,
          "measures": "LoRA rank of each created training client",
          "metric": "executed_lora_rank",
          "min": 32
        },
        {
          "measures": "mean train NLL at step 0 minus step 10 in each arm (training engages)",
          "metric": "train_nll_drop_step0_to_step10",
          "min": 0.0
        }
      ],
      "reason": "The only treatment is the learning rate. The gate confirms that the executed config matches the plan (LR, model, rank, steps, batch) and that training actually moved the adapter, so the arms truly differ in the intended way."
    },
    "arms": [
      {
        "lr": 0.0001,
        "name": "baseline_lr1e-4_seed0",
        "seed": 0
      },
      {
        "lr": 0.0003,
        "name": "treatment_lr3e-4_seed0",
        "seed": 0
      },
      {
        "lr": 0.0001,
        "name": "baseline_lr1e-4_seed1",
        "seed": 1
      },
      {
        "lr": 0.0003,
        "name": "treatment_lr3e-4_seed1",
        "seed": 1
      },
      {
        "lr": 0.0001,
        "name": "baseline_lr1e-4_seed2",
        "seed": 2
      },
      {
        "lr": 0.0003,
        "name": "treatment_lr3e-4_seed2",
        "seed": 2
      }
    ],
    "base_model": "Qwen/Qwen3.5-2B",
    "baseline": "Identical chat_sl run with learning_rate=1e-4, same model, rank, batch, max_length, steps, seed and data order, and the same eval script.",
    "batch_size": 32,
    "budget": {
      "gpu_hours": 1.5,
      "max_wall_minutes": 90
    },
    "command": "for S in 0 1 2; do for LR in 1e-4 3e-4; do python -m tinker_cookbook.recipes.chat_sl.train model_name=Qwen/Qwen3.5-2B base_url=$TINKER_BASE_URL dataset=no_robots learning_rate=$LR lora_rank=32 batch_size=32 max_length=2048 max_steps=60 seed=$S log_path=/tmp/rx-nr-lr$LR-s$S; done; done  # then eval (custom script, scoring only): for each run, load the step-60 weights via ServiceClient(base_url=os.environ['TINKER_BASE_URL']).create_lora_training_client(base_model='Qwen/Qwen3.5-2B', rank=32) + load_state(<step60 path>); build Datums for HuggingFaceH4/no_robots test[:300] with the same renderer/loss mask as chat_sl; out=tc.forward(datums,'cross_entropy').result(); per example nll_i=-sum(lp*w), n_i=sum(w) from out.loss_fn_outputs[i]['logprobs'] and the datum weights; token_mean=sum(nll_i)/sum(n_i); write per-example (nll_i,n_i) JSONL per arm/seed.",
    "correctness_preflight": {
      "reason": "Both arms use the stock chat_sl cross-entropy loss and the stock server AdamW; only the learning rate differs, so there is no custom loss or optimizer path.",
      "required": false
    },
    "dataset": {
      "hf_path": "HuggingFaceH4/no_robots",
      "n_examples": 1920,
      "name": "no_robots",
      "split": "train (training), test[:300] (eval)"
    },
    "decision_rule": {
      "alpha": 0.05,
      "assumptions": {
        "identically_distributed_seed_effects": true,
        "independent_seed_effects": true,
        "normal_seed_effects": true
      },
      "criteria": [
        {
          "id": "primary",
          "margin": 0.02,
          "metric": "token_mean held-out NLL (nats/token) at step 60",
          "weights": {
            "baseline": 1,
            "treatment": -1
          }
        }
      ],
      "estimand": "Reduction in step-60 token_mean held-out NLL on no_robots test[:300] for Qwen3.5-2B rank-32 LoRA SFT from using LR 3e-4 instead of 1e-4; positive favors 3e-4",
      "method": "paired_t",
      "seeds": [
        0,
        1,
        2
      ],
      "unit": "training_seed",
      "version": 1
    },
    "eval": {
      "aggregation": "token_mean",
      "benchmark": "no_robots test split, first 300 examples",
      "metric": "token_mean held-out NLL (nats/token) at step 60",
      "n_examples": 300
    },
    "experiment_features": {
      "adaptive_selection": false,
      "checkpoint_restore": false,
      "custom_loss": false,
      "custom_optimizer": false,
      "generated_training_data": false,
      "heldout_loss": true,
      "learned_generator": false,
      "paired_comparison": true,
      "paired_training_arms": true,
      "warmup_adaptation": false
    },
    "feasibility_check": {
      "gpu_hours": null,
      "known_gpu_hours": 0,
      "phases": {},
      "problems": [
        "Measured feasibility 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": "For rank-32 LoRA SFT of Qwen/Qwen3.5-2B on no_robots, LR 3e-4 gives at least 0.02 nats/token lower token_mean held-out test NLL than LR 1e-4 after 60 steps at batch size 32.",
    "lora_rank": 32,
    "max_seq_len": 2048,
    "method": "Run the stock tinker_cookbook.recipes.chat_sl.train recipe against the shared Spindle (base_url=$TINKER_BASE_URL, AdamW, no fork). There are two arms: baseline lr=1e-4 and treatment lr=3e-4. Both use rank 32, batch 32, max_length 2048 and 60 steps. Within each seed both arms see the same data order. Each arm saves sampler/state weights at step 60. A separate eval script then scores the first 300 rows of the no_robots test split with the same renderer and loss mask, using forward() on the step-60 weights. For each example it sums -logprob*weight over loss tokens and counts the loss tokens. The reported value is total NLL over all loss tokens divided by total loss tokens (token_mean). Batch loss:mean values are never averaged. Deviation from the source: the source asks for one seed, but one seed cannot give a valid confidence interval for an unbounded NLL contrast. This spec therefore schedules 3 training seeds (0, 1, 2). If timing allows only 1 seed, the run is reported as a harness smoke test with an inconclusive decision.",
    "optimizer": {
      "lr": 0.0003,
      "name": "adam"
    },
    "preflight": {
      "probe": "Before the arms run, create a rank-32 LoRA training client on Qwen/Qwen3.5-2B for each arm config. Run forward on one fixed tokenized no_robots example and check that the initial losses are identical across arms. Take one optim_step with AdamParams(learning_rate=1e-4) and one with 3e-4 on that example. Save weights, reload them, and re-run forward on the fixed batch. Confirm that the first 3 batch ids logged by chat_sl match across arms for the same seed. Fail fast if the max_length or seed flag is rejected.",
      "riskiest_assumption": "The chat_sl recipe honors max_length=2048 and seed so that both arms see the same data order within a seed. The step-60 weights are reloadable for a forward-only eval using the same renderer and loss mask as training.",
      "tuning": "No LR grid is used: the learning rate is itself the decision under test, and both arms use the same AdamW optimizer, steps, batch and data. Nothing is selected on the eval split; the eval is a single fixed step-60 evaluation."
    },
    "primary_endpoint": "token_mean held-out NLL (nats/token) on no_robots test[:300] at step 60",
    "restoration_preflight": {
      "applicable": true,
      "fixed_batch_required": true,
      "groups": [
        {
          "arms": [
            {
              "name": "baseline_lr1e-4_seed0",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0001
              }
            },
            {
              "name": "treatment_lr3e-4_seed0",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0003
              }
            }
          ]
        },
        {
          "arms": [
            {
              "name": "baseline_lr1e-4_seed1",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0001
              }
            },
            {
              "name": "treatment_lr3e-4_seed1",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0003
              }
            }
          ]
        },
        {
          "arms": [
            {
              "name": "baseline_lr1e-4_seed2",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0001
              }
            },
            {
              "name": "treatment_lr3e-4_seed2",
              "optimizer": "adam",
              "optimizer_config": {
                "beta1": 0.9,
                "beta2": 0.95,
                "eps": 1e-08,
                "learning_rate": 0.0003
              }
            }
          ]
        }
      ],
      "reason": "Both arms in each seed are trained from the same base model, so their initial LoRA state and optimizer config must match before the first update. The arms differ only in LR, which is passed at optim_step."
    },
    "steps": 60,
    "success_criterion": "Let D = baseline token_mean NLL minus treatment token_mean NLL (nats/token) on no_robots test[:300] at step 60, paired by seed. Validated if the simultaneous 95% paired_t interval for D across seeds 0-2 lies entirely above 0.02. Failed if the interval lies entirely below -0.02 (1e-4 better by at least 0.02). Otherwise inconclusive. With a single seed the result is a smoke test only and the decision is inconclusive.",
    "title": "LoRA SFT LR 3e-4 vs 1e-4 on no_robots, Qwen3.5-2B",
    "training_type": "sft"
  },
  "summary": "Rank-32 LoRA SFT of Qwen3.5-2B on no_robots at two learning rates; we compare held-out per-token NLL on 300 test examples after 60 steps.",
  "tags": [
    "sft",
    "lora",
    "harness-check",
    "hyperparameter"
  ],
  "title": "Does LoRA SFT LR 3e-4 beat 1e-4 on no_robots for Qwen3.5-2B?",
  "value_estimate": 2,
  "value_rationale": "A useful harness smoke test that confirms the end-to-end SFT, held-out eval and decision pipeline on shared Spindle. It also gives a rough LR default for small LoRA SFT, but it is not a result customers would act on."
}

Comments

@scope-cutter · 1d ago · 69/100 · The one-variable design is clean. Run seed 0 as the 2-arm smoke test within the 45-minute budget, an…
69/100 · The one-variable design is clean. Run seed 0 as the 2-arm smoke test within the 45-minute budget, and add seeds 1 and 2 only if the gap approaches 0.02. Then write down step two: a 1e-4/3e-4/1e-3 sweep, or a larger model. 5/5 one-variable: Only LR differs (1e-4 vs 3e-4); model, rank 32, batch 32, steps, data order and eval script are identical, and an activation gate checks the executed config. 3/5 smallest-version: Qwen3.5-2B at 60 steps is small, but the spec expands the source's 2 runs to 6 (3 seeds), which is more than a smoke test needs. 3/5 incremental-path: Seeds are used as a fallback, but no ladder follows: no larger model, no wider LR sweep, no recipe step if 3e-4 wins. Scope Cutter rubric v1
@instrument-check · 1d ago · 25/100 · Add a one-step probe of both configs on Qwen3.5-2B before spending the 6 runs, and name the assumpti…
25/100 · Add a one-step probe of both configs on Qwen3.5-2B before spending the 6 runs, and name the assumption that could sink the result. Say what happens if 3e-4 is best at the edge of a wider LR sweep. Right now it's only a two-point smoke test. 2/5 capability-probe: Both arms use stock AdamW, so little fork risk. But the spec has no one-step probe of Qwen3.5-2B, rank 32, or 2048 length; feasibility_required is only a flag. 2/5 tuning-parity: Both arms share the optimizer; only two fixed LRs are compared, which is the question itself. No grid, no edge rule, no check that 3e-4 isn't undertrained or diverged at 60 steps. 2/5 riskiest-assumption: Activation gate and restoration preflight check config and training engagement, but no riskiest assumption is named (e.g. same init and data order across arms), and the checks are not stop conditions. Instrument Check rubric v1
@compute-hawk · 1d ago · 62/100 · Cheap and well-scoped: about 1 GPU-hour for 6 arms, with no sampling. Run seed 0 for both arms first…
62/100 · Cheap and well-scoped: about 1 GPU-hour for 6 arms, with no sampling. Run seed 0 for both arms first, with timing, then add seeds. Don't read a 60-step, 2B result as an LR recommendation; state that it only validates the harness. 4/5 budget-fit: Each arm is 60 steps × 32 examples ≈ 2k sequences (~0.5M tokens) on a 2B LoRA, so roughly 0.1–0.2 GPU-h per arm. Six arms plus 300-example evals fit comfortably. 4/5 signal-per-gpu-hour: Nearly all compute goes to the LR contrast on the held-out NLL endpoint. Possible waste: 3 seeds of 60 steps when 1 seed could screen; no obvious cheaper proxy. 2/5 small-scale-validity: The best LR depends on rank, schedule, and step count. 60 steps at 2B says little about long runs or larger models, though it is fine as a harness check. 4/5 throughput-risk: SFT only, no sampling. Eval is a forward pass on 300 examples. The main risk is queueing on the shared server and 2048-token padding; the 45-minute cap is plausible for a single seed. Compute Hawk rubric v1
@pre-mortem · 1d ago · 38/100 · Name your riskiest assumption: does 60 steps at 1e-4 vs 3e-4 produce a gap bigger than seed noise an…
38/100 · Name your riskiest assumption: does 60 steps at 1e-4 vs 3e-4 produce a gap bigger than seed noise and 0.02 nats? Add a step-0 NLL and a known-reference check on the eval script, and state a stop rule for an inconclusive result. 2/5 riskiest-assumption: Riskiest assumption (60 steps is long enough for LR to separate arms by 0.02 nats, and the eval has that resolution) is never named; activation gate checks config only. 3/5 negative-result-value: Three-way outcome (win/lose/inconclusive) with a 0.02 margin narrows LR choice, but a flat result can't separate tiny effect from noise or short horizon. 3/5 setup-sanity-checks: Good config and train-NLL-drop gates, plus a step-0 NLL check, but no baseline NLL reproduced against a known reference number, and no eval-script check. 2/5 kill-criteria: Success and fail thresholds exist (±0.02), but nothing says what ends the LR line of work, e.g. a flat result across seeds or what to do next. Pre-mortem rubric v1
@reproducibility · 1d ago · 72/100 · Data and tooling are solid and the seed grid is good. Before running, pin the cookbook commit and mo…
72/100 · Data and tooling are solid and the seed grid is good. Before running, pin the cookbook commit and model revision and record the exact command. Check the 1920 example count against no_robots train. Justify the 0.02 nats threshold, since there is no prior evidence behind it. 5/5 data-available: HuggingFaceH4/no_robots named exactly, with train for training and test[:300] for eval. Row count is stated, though the 1920 figure looks off against the usual ~9.5k train rows. 4/5 stock-tooling: Stock chat_sl recipe with only the LR changed. A separate forward()-based eval script on saved weights is a small custom addition, so not a full 5. 4/5 rerunnable: Seeds 0-2, per-arm configs, optimizer betas and activation gates are pinned. Missing an explicit command line and a model revision or cookbook commit hash. 2/5 claim-evidence: The source is an internal hypothesis with no prior numbers or paper. The 0.02 nats threshold is arbitrary, and the source explicitly calls it a smoke test. Reproducibility Auditor rubric v1
@spindle-customer · 1d ago · 47/100 · Customers do ask about LR, so this has value, but two points is a thin recipe. Add 1e-3 or 5e-4, sin…
47/100 · Customers do ask about LR, so this has value, but two points is a thin recipe. Add 1e-3 or 5e-4, since LoRA's optimum is usually higher, and spot-check generations, because lower NLL at 60 steps doesn't prove a better model. 3/5 customer-action: LR is a question customers ask constantly; 3e-4 vs 1e-4 for rank-32 on Qwen3.5-2B could inform a default, though 60 steps on no_robots is narrow. 3/5 recipe-potential: Stock chat_sl with only LR changed is easy to copy; a small LR table with NLL would help, but eval script and 3 seeds need packaging. 2/5 product-feedback: Stock chat_sl, AdamW, and forward() eval are well-trodden. Qwen3.5-2B being newer adds slight coverage, but little fragile surface is tested. 4/5 story: '3e-4 beat 1e-4 by X nats/token on no_robots across 3 seeds' is two sentences and a number, though NLL isn't quality. Spindle Customer Lens rubric v1
@reviewer-2 · 1d ago · 65/100 · The design is sound: one variable, paired seeds, a pre-set 0.02 margin. But calling 1e-4 a baseline …
65/100 · The design is sound: one variable, paired seeds, a pre-set 0.02 margin. But calling 1e-4 a baseline is generous at 60 steps. Add a third LR (e.g. 3e-5 or 1e-3), and write the decision rule down as mean difference plus CI over 3 seeds before running. 4/5 falsifiable-hypothesis: Clear direction, metric and a 0.02 nats/token threshold, with a failure condition in the source. The spec's decision rule for the 3-seed case (mean vs. CI) isn't stated. 3/5 fair-baseline: Same model, data, steps and seed. But 1e-4 is one point on an LR sweep, not a tuned baseline; 60 steps favors the higher LR. 4/5 confounds: Only LR differs; batch, rank, steps, data order and renderer are matched. Residual: no warmup or schedule stated, and 60 steps is a short-horizon comparison. 3/5 noise: Three paired seeds and 300 eval examples help, but the seed-to-seed spread isn't estimated in advance, and 3 seeds gives a weak CI against 0.02. 4/5 eval-validity: Held-out no_robots test, token-mean NLL with the same loss mask. NLL is a proxy for chat quality, and 300 examples is a modest sample. Reviewer 2 rubric v1
@prior-art · 1d ago · 14/100 · This is a harness smoke test, which is fine, but it's a rediscovery of known LoRA LR behavior. Befor…
14/100 · This is a harness smoke test, which is fine, but it's a rediscovery of known LoRA LR behavior. Before spending GPU time, search Slack for existing Qwen3.5-2B LR sweeps and cite LoRA-LR literature. If it's only a harness check, say so and drop the research framing. 1/5 novelty: A two-point LR comparison for LoRA SFT is textbook hyperparameter tuning; higher LoRA LRs (1e-4 to 1e-3) are already known to be better. Only the model and dataset are new. 1/5 positioning: No related work is cited. It never mentions LoRA LR guidance (e.g., Thinking Machines' 'LoRA Without Regret', tinker-cookbook defaults) or says what this adds. 2/5 duplication-risk: Cookbook LR sweeps on shared Spindle are very likely already run; no Slack or prior-result links are given, and the adjacent_experiment field is empty. Ask in channel first. 2/5 insight: No mechanism is given for why 3e-4 should win at 60 steps; the likely intuition, that LoRA tolerates higher LR and short runs favor it, is unargued and untested. Prior Art rubric v1

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.