CLAUDE.md states the core invariant: "Backends dispatch on the spec's fields, never on the method name." The verl backend breaks it, and also breaks the "ignored with a log line" convention for TrainConfig.
-
Name dispatch. backends/verl.py:142 does if config.method != "grpo": raise. It should check spec.trainer != "grpo". As written, a user-registered method with trainer="grpo" (the documented methods.register(...) extension point) is rejected by verl even though it is a valid GRPO method.
-
Silent TrainConfig drops. verl.py:43-57 maps roughly 7 of ~35 fields (grpo_max_completion_length, max_seq_length, learning_rate, beta, grpo_group_size, derived total). Everything else is discarded with no log line: weight_decay, max_grad_norm, lr_scheduler_type, optim, warmup fields, batch/accumulation, seed, save_steps/eval_steps/logging_steps, report_to, packing, use_rslora, resume_from_checkpoint, and the whole LoRA family (verl GRPO is full-parameter). The SHADOWLM_VERL_OVERRIDES escape hatch at verl.py:64 is undiscoverable from a run log. The convention says each ignored field gets a log line; mlx already implements the pattern at backends/mlx.py:349-358.
-
Stub load, missing surface. verl.py:133-137 load() only records the model name and silently discards load_in_4bit and max_seq_length. generate() raises (verl.py:198, with an actionable message), but chat() and save() fall through to the generic backends/base.py:96,99 raises with no actionable message, so backend="verl" cannot round-trip the SDK surface at all.
-
Minor: verl.py:50 formats config.learning_rate:g, which is a TypeError when None. Both live paths resolve the method default first (models.py:196, serve.py:148), but a direct Backend.finetune() call hits it.
CLAUDE.md states the core invariant: "Backends dispatch on the spec's fields, never on the method name." The verl backend breaks it, and also breaks the "ignored with a log line" convention for TrainConfig.
Name dispatch.
backends/verl.py:142doesif config.method != "grpo": raise. It should checkspec.trainer != "grpo". As written, a user-registered method withtrainer="grpo"(the documentedmethods.register(...)extension point) is rejected by verl even though it is a valid GRPO method.Silent TrainConfig drops.
verl.py:43-57maps roughly 7 of ~35 fields (grpo_max_completion_length,max_seq_length,learning_rate,beta,grpo_group_size, derived total). Everything else is discarded with no log line:weight_decay,max_grad_norm,lr_scheduler_type,optim, warmup fields, batch/accumulation,seed,save_steps/eval_steps/logging_steps,report_to,packing,use_rslora,resume_from_checkpoint, and the whole LoRA family (verl GRPO is full-parameter). TheSHADOWLM_VERL_OVERRIDESescape hatch atverl.py:64is undiscoverable from a run log. The convention says each ignored field gets a log line; mlx already implements the pattern atbackends/mlx.py:349-358.Stub load, missing surface.
verl.py:133-137 load()only records the model name and silently discardsload_in_4bitandmax_seq_length.generate()raises (verl.py:198, with an actionable message), butchat()andsave()fall through to the genericbackends/base.py:96,99raises with no actionable message, sobackend="verl"cannot round-trip the SDK surface at all.Minor:
verl.py:50formatsconfig.learning_rate:g, which is aTypeErrorwhenNone. Both live paths resolve the method default first (models.py:196,serve.py:148), but a directBackend.finetune()call hits it.