evalrx 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""L3b fix executors — internals-**modifying** repairs (the sandbox boundary).
|
|
2
|
+
|
|
3
|
+
This module holds only what genuinely cannot be handed to a sandboxed coding
|
|
4
|
+
agent: pre-audited, parameterised primitives that **write** to the forward pass.
|
|
5
|
+
|
|
6
|
+
* **L3a (internals, read)** is intentionally NOT here at all. Reading attention
|
|
7
|
+
needs no privileged model handle, so the capability is exposed to sandboxed
|
|
8
|
+
coded pipelines via ``model_attend()`` (see :mod:`fix_pipeline`), and the
|
|
9
|
+
agent writes its own peak-find → crop → re-ask scaffold. The host-side
|
|
10
|
+
capture that backs ``model_attend()`` is
|
|
11
|
+
:func:`~evalrx.analyzers.attention.relative_attn.attention_heatmap` — a
|
|
12
|
+
generic attention reducer that lives with the analyzers (one reduction shared
|
|
13
|
+
with the white-box probe path), not in this fix module. (An earlier
|
|
14
|
+
``attention_guided_crop`` primitive was removed for the same reason — it
|
|
15
|
+
duplicated what the coded path already writes.)
|
|
16
|
+
* **L3b (internals, write)**: pre-audited intervention primitives that modify the
|
|
17
|
+
forward pass — **never** free codegen against the model handle, because
|
|
18
|
+
arbitrary hook code with the raw model object cannot be sandboxed. The judge
|
|
19
|
+
selects and parameterises; it never authors these. v1 ships *visual embedding
|
|
20
|
+
boost*: a forward hook on the input-embedding layer scaling image-token
|
|
21
|
+
embeddings by ``gamma`` (architecture-agnostic for HF VLMs whose image tokens
|
|
22
|
+
are placeholder ids in ``input_ids``). Attention-map editing needs
|
|
23
|
+
per-architecture hooks and joins this registry later.
|
|
24
|
+
|
|
25
|
+
L4 (parameter space): :class:`FinetuneSpec` captures a complete fine-tune
|
|
26
|
+
recipe (dataset construction generalising the verified hypothesis, method,
|
|
27
|
+
target, evaluation protocol incl. a regression battery). v1 of the executor
|
|
28
|
+
(:func:`run_lora_repair`) executes exactly one recipe shape — ``method="lora"``
|
|
29
|
+
on ``target="llm"`` — and refuses everything else with a descriptive reason
|
|
30
|
+
rather than guessing:
|
|
31
|
+
|
|
32
|
+
* ``dataset_recipe`` (free natural-language text) is **never interpreted**.
|
|
33
|
+
There is no safe path from an arbitrary judge-written string to executable
|
|
34
|
+
data synthesis without another judge call producing code — the same
|
|
35
|
+
structured-output path whose failures cost three real bugs elsewhere in
|
|
36
|
+
this project (see the ``vlm_paper_benchmark`` example's README). Training
|
|
37
|
+
data is instead a fixed, defensible default: diagnosis-split failing cases
|
|
38
|
+
as ``(prompt, image) -> gold answer`` SFT pairs, plus a sample of
|
|
39
|
+
diagnosis-split *passing* cases as anti-forgetting ballast — supplied by
|
|
40
|
+
the caller via a pool that must be disjoint from whatever batch is being
|
|
41
|
+
validated (see :class:`~.fix_agent.FixAgent`'s ``finetune_pool``).
|
|
42
|
+
* ``method in {"sft", "full"}`` and ``target in {"vision_encoder",
|
|
43
|
+
"projector"}`` are recorded (for the escalation decision) but not
|
|
44
|
+
executed — the former needs a real SFT training loop rather than a
|
|
45
|
+
handful of LoRA steps, the latter needs per-architecture module-naming
|
|
46
|
+
conventions this project has no way to validate without a training run
|
|
47
|
+
per architecture. Both are TODO.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import logging
|
|
53
|
+
from contextlib import contextmanager
|
|
54
|
+
from dataclasses import dataclass, field
|
|
55
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
56
|
+
|
|
57
|
+
from evalrx.eval_agent.stages.fix_tiers import FixTier
|
|
58
|
+
from evalrx.eval_agent.stages.fix_tools import score_to_bool
|
|
59
|
+
|
|
60
|
+
if TYPE_CHECKING:
|
|
61
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
62
|
+
from evalrx.core.model import Model
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
ScoreFn = Callable[["FailureCase", str], Optional[bool]]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# L3b — visual embedding boost (forward-hook intervention)
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def _resolve_hf(model: "Model"):
|
|
74
|
+
"""Underlying (HF model, image_token_id) or (None, None) when unavailable."""
|
|
75
|
+
hf = getattr(model, "_hf", None)
|
|
76
|
+
hf_model = hf[0] if isinstance(hf, tuple) and hf else None
|
|
77
|
+
if hf_model is None:
|
|
78
|
+
return None, None
|
|
79
|
+
cfg = getattr(hf_model, "config", None)
|
|
80
|
+
for attr in ("image_token_id", "image_token_index"):
|
|
81
|
+
tid = getattr(cfg, attr, None)
|
|
82
|
+
if tid is not None:
|
|
83
|
+
return hf_model, int(tid)
|
|
84
|
+
return None, None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def boost_available(model: "Model") -> bool:
|
|
88
|
+
return _resolve_hf(model)[0] is not None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@contextmanager
|
|
92
|
+
def visual_embedding_boost(model: "Model", gamma: float = 1.5):
|
|
93
|
+
"""Scale image-token embeddings by *gamma* for every forward inside the block."""
|
|
94
|
+
hf_model, image_token_id = _resolve_hf(model)
|
|
95
|
+
if hf_model is None:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"visual_embedding_boost: backend internals unavailable "
|
|
98
|
+
"(needs a loaded hf_local model exposing image_token_id)"
|
|
99
|
+
)
|
|
100
|
+
embedding = hf_model.get_input_embeddings()
|
|
101
|
+
gamma = float(gamma)
|
|
102
|
+
|
|
103
|
+
def _hook(module, args, output):
|
|
104
|
+
input_ids = args[0]
|
|
105
|
+
mask = input_ids == image_token_id
|
|
106
|
+
if mask.any():
|
|
107
|
+
output = output.clone()
|
|
108
|
+
output[mask] = output[mask] * gamma
|
|
109
|
+
return output
|
|
110
|
+
|
|
111
|
+
handle = embedding.register_forward_hook(_hook)
|
|
112
|
+
try:
|
|
113
|
+
yield
|
|
114
|
+
finally:
|
|
115
|
+
handle.remove()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_visual_embedding_boost(
|
|
119
|
+
model: "Model",
|
|
120
|
+
cases: "CaseBatch",
|
|
121
|
+
score_fn: ScoreFn,
|
|
122
|
+
params: "dict[str, Any] | None" = None,
|
|
123
|
+
) -> "dict[str, Optional[bool]]":
|
|
124
|
+
"""Generate every case under the boost hook; score against the rubric."""
|
|
125
|
+
gamma = float((params or {}).get("gamma", 1.5))
|
|
126
|
+
scores: "dict[str, Optional[bool]]" = {}
|
|
127
|
+
try:
|
|
128
|
+
with visual_embedding_boost(model, gamma=gamma):
|
|
129
|
+
for case in cases:
|
|
130
|
+
try:
|
|
131
|
+
out = str(model.generate(case.inputs))
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.debug("boosted generate failed on %s: %s", case.id, exc)
|
|
134
|
+
scores[case.id] = None
|
|
135
|
+
continue
|
|
136
|
+
scores[case.id] = score_to_bool(score_fn(case, out))
|
|
137
|
+
except RuntimeError as exc:
|
|
138
|
+
logger.warning("visual_embedding_boost unavailable: %s", exc)
|
|
139
|
+
return {c.id: None for c in cases}
|
|
140
|
+
return scores
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Primitive registry (judge selects + parameterises; code is pre-audited)
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class InternalsPrimitive:
|
|
149
|
+
"""A host-side, pre-audited L3 intervention primitive."""
|
|
150
|
+
|
|
151
|
+
name: str
|
|
152
|
+
tier: FixTier
|
|
153
|
+
description: str
|
|
154
|
+
params_hint: str
|
|
155
|
+
available: "Callable[[Model], bool]"
|
|
156
|
+
run: "Callable[[Model, CaseBatch, ScoreFn, dict | None], dict[str, Optional[bool]]]"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
#: Pre-audited internals-WRITE primitives only. Reads (L3a) are not here — the
|
|
160
|
+
#: agent authors them against the ``model_attend()`` bridge (see module docstring).
|
|
161
|
+
INTERNALS_PRIMITIVES: "dict[str, InternalsPrimitive]" = {
|
|
162
|
+
"visual_embedding_boost": InternalsPrimitive(
|
|
163
|
+
name="visual_embedding_boost",
|
|
164
|
+
tier=FixTier.L3B_INTERNALS_WRITE,
|
|
165
|
+
description="scale image-token embeddings by gamma via a forward hook "
|
|
166
|
+
"(amplifies visual evidence against language priors)",
|
|
167
|
+
params_hint='{"gamma": float > 1 (default 1.5)}',
|
|
168
|
+
available=boost_available,
|
|
169
|
+
run=run_visual_embedding_boost,
|
|
170
|
+
),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def primitives_catalog_text(model: "Model", max_tier: FixTier) -> str:
|
|
175
|
+
"""Render available primitives (≤ *max_tier*, supported by *model*)."""
|
|
176
|
+
lines = []
|
|
177
|
+
for prim in INTERNALS_PRIMITIVES.values():
|
|
178
|
+
if prim.tier <= max_tier and prim.available(model):
|
|
179
|
+
lines.append(f"- {prim.name} [{prim.tier.label}]: {prim.description} "
|
|
180
|
+
f"[params: {prim.params_hint}]")
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# L4 — parameter space: recipe dataclass + v1 LoRA executor
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class FinetuneSpec:
|
|
190
|
+
"""A complete L4 fine-tune recipe.
|
|
191
|
+
|
|
192
|
+
Executed by :func:`run_lora_repair` when ``method == "lora"`` and
|
|
193
|
+
``target == "llm"`` and the caller supplied a ``finetune_pool``; recorded
|
|
194
|
+
without running otherwise (TODO: SFT training loop for ``method="sft"``/
|
|
195
|
+
``"full"``; per-architecture module-naming for ``target="vision_encoder"``/
|
|
196
|
+
``"projector"``). :class:`~.fix_agent.FixAgent` always records the recipe
|
|
197
|
+
as a candidate either way, so the escalation decision has something
|
|
198
|
+
concrete to act on even when it wasn't executed.
|
|
199
|
+
|
|
200
|
+
Attributes:
|
|
201
|
+
dataset_recipe: How to build training data that *generalises* the
|
|
202
|
+
verified mechanism (never just the failing cases).
|
|
203
|
+
method: Training method, e.g. ``"lora"`` / ``"sft"``.
|
|
204
|
+
target: Component to tune, e.g. ``"vision_encoder"`` /
|
|
205
|
+
``"llm"`` / ``"projector"`` / ``"full"``.
|
|
206
|
+
eval_protocol: How the tuned model must be validated — held-out
|
|
207
|
+
repair effect AND no-regression on passing cases.
|
|
208
|
+
rationale: Why parameter-space change is the minimum effective
|
|
209
|
+
intervention for the hypothesis.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
dataset_recipe: str
|
|
213
|
+
method: str = "lora"
|
|
214
|
+
target: str = "llm"
|
|
215
|
+
eval_protocol: str = (
|
|
216
|
+
"paired McNemar on held-out failures + regression battery on all "
|
|
217
|
+
"baseline-passing cases"
|
|
218
|
+
)
|
|
219
|
+
rationale: str = ""
|
|
220
|
+
metadata: "dict[str, Any]" = field(default_factory=dict)
|
|
221
|
+
|
|
222
|
+
def to_dict(self) -> "dict[str, Any]":
|
|
223
|
+
return {
|
|
224
|
+
"dataset_recipe": self.dataset_recipe,
|
|
225
|
+
"method": self.method,
|
|
226
|
+
"target": self.target,
|
|
227
|
+
"eval_protocol": self.eval_protocol,
|
|
228
|
+
"rationale": self.rationale,
|
|
229
|
+
"metadata": self.metadata,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class LoraRepairResult:
|
|
235
|
+
"""Outcome of one L4 LoRA train-and-validate cycle.
|
|
236
|
+
|
|
237
|
+
Mirrors :class:`~.fix_pipeline.CodedPipelineResult`'s shape (``scores``/
|
|
238
|
+
``ok``/``error``) so the caller's ``exec_error`` accounting is identical
|
|
239
|
+
across coded and fine-tune candidates.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
scores: "dict[str, Optional[bool]]" = field(default_factory=dict)
|
|
243
|
+
ok: bool = False
|
|
244
|
+
error: str = ""
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def lora_available() -> bool:
|
|
248
|
+
"""Whether the optional ``peft`` dependency (``pip install evalrx[finetune]``)
|
|
249
|
+
is installed. ``peft`` hard-depends on ``transformers``, so this single
|
|
250
|
+
check also covers that."""
|
|
251
|
+
try:
|
|
252
|
+
import peft # noqa: F401
|
|
253
|
+
except ImportError:
|
|
254
|
+
return False
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _lora_target_modules(hf_model: Any) -> "str | list[str]":
|
|
259
|
+
"""Scope LoRA to the language-model component (v1 only ever executes
|
|
260
|
+
``target="llm"``).
|
|
261
|
+
|
|
262
|
+
When the model exposes a ``language_model`` submodule (the same
|
|
263
|
+
convention :func:`~.hf_local.HFLocalModel.generate_ifcd` and
|
|
264
|
+
``generate_pai`` already use for "the LLM half of a VLM"), a regex
|
|
265
|
+
anchored to that dotted prefix is used — plain name-suffix matching
|
|
266
|
+
(``q_proj``/``k_proj``/...) is not safe here, because CLIP-style vision
|
|
267
|
+
towers commonly use the *same* projection names, and a suffix-only match
|
|
268
|
+
would silently pull the vision encoder into a candidate whose contract
|
|
269
|
+
promises ``target="llm"`` only. Falls back to plain suffix matching for a
|
|
270
|
+
text-only backbone, where there is no vision tower to leak into.
|
|
271
|
+
"""
|
|
272
|
+
suffixes = ("q_proj", "k_proj", "v_proj", "o_proj")
|
|
273
|
+
if getattr(hf_model, "language_model", None) is not None:
|
|
274
|
+
return r"language_model\..*\.(" + "|".join(suffixes) + ")"
|
|
275
|
+
return list(suffixes)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _build_sft_examples(
|
|
279
|
+
train_cases: "CaseBatch", max_examples: int
|
|
280
|
+
) -> "list[tuple[FailureCase, str]]":
|
|
281
|
+
"""Diagnosis-split-only SFT pairs — never interprets ``dataset_recipe``
|
|
282
|
+
(see module docstring). Failing cases (gold answer as the SFT target)
|
|
283
|
+
teach the correction; a matched sample of already-passing cases is
|
|
284
|
+
anti-forgetting ballast. The caller (:class:`~.fix_agent.FixAgent`) is
|
|
285
|
+
responsible for ``train_cases`` being disjoint from whatever batch is
|
|
286
|
+
being validated — this function has no way to detect contamination.
|
|
287
|
+
"""
|
|
288
|
+
from evalrx.core.case import Label
|
|
289
|
+
|
|
290
|
+
def _usable(case: "FailureCase") -> bool:
|
|
291
|
+
return case.expected not in (None, "") and bool(str(case.inputs.prompt or "").strip())
|
|
292
|
+
|
|
293
|
+
fail = [(c, str(c.expected)) for c in train_cases if c.label == Label.FAIL and _usable(c)]
|
|
294
|
+
passed = [(c, str(c.expected)) for c in train_cases if c.label == Label.PASS and _usable(c)]
|
|
295
|
+
if not fail:
|
|
296
|
+
return []
|
|
297
|
+
budget = max(1, int(max_examples))
|
|
298
|
+
n_fail = min(len(fail), budget)
|
|
299
|
+
n_pass = min(len(passed), max(0, budget - n_fail))
|
|
300
|
+
return fail[:n_fail] + passed[:n_pass]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _encode_sft_example(
|
|
304
|
+
processor: Any, prompt: str, image: Any, target_text: str, chat_template_kwargs: "dict[str, Any]"
|
|
305
|
+
) -> "dict[str, Any]":
|
|
306
|
+
"""One teacher-forced training example: everything up to (and including)
|
|
307
|
+
the prompt is masked to ``-100``; loss applies only to the target-answer
|
|
308
|
+
tokens.
|
|
309
|
+
|
|
310
|
+
Uses the processor's chat template when available (production VLM/LLM
|
|
311
|
+
processors); falls back to plain ``prompt + " " + target`` concatenation
|
|
312
|
+
otherwise (simple tokenizers, InstructBLIP-style processors with no chat
|
|
313
|
+
template — its ``Question:``/``Answer:`` framing is not reproduced here,
|
|
314
|
+
a v1 simplification). The prompt/target boundary is found by tokenizing
|
|
315
|
+
the prompt-only prefix separately and using its length as the mask
|
|
316
|
+
cutoff: an approximation (BPE merges spanning the boundary can shift a
|
|
317
|
+
token or two) accepted as good enough for v1.
|
|
318
|
+
"""
|
|
319
|
+
tok = getattr(processor, "tokenizer", processor)
|
|
320
|
+
chat_template = getattr(tok, "chat_template", None)
|
|
321
|
+
images = None
|
|
322
|
+
if image is not None:
|
|
323
|
+
images = list(image) if isinstance(image, (list, tuple)) else [image]
|
|
324
|
+
|
|
325
|
+
if chat_template:
|
|
326
|
+
content: Any = (
|
|
327
|
+
[{"type": "image"} for _ in images] + [{"type": "text", "text": prompt}]
|
|
328
|
+
if images else prompt
|
|
329
|
+
)
|
|
330
|
+
prefix_text = processor.apply_chat_template(
|
|
331
|
+
[{"role": "user", "content": content}],
|
|
332
|
+
add_generation_prompt=True, tokenize=False, **chat_template_kwargs,
|
|
333
|
+
)
|
|
334
|
+
full_text = processor.apply_chat_template(
|
|
335
|
+
[
|
|
336
|
+
{"role": "user", "content": content},
|
|
337
|
+
{"role": "assistant", "content": target_text},
|
|
338
|
+
],
|
|
339
|
+
add_generation_prompt=False, tokenize=False, **chat_template_kwargs,
|
|
340
|
+
)
|
|
341
|
+
if images:
|
|
342
|
+
prefix_enc = processor(text=[prefix_text], images=images, return_tensors="pt")
|
|
343
|
+
full_enc = processor(text=[full_text], images=images, return_tensors="pt")
|
|
344
|
+
else:
|
|
345
|
+
prefix_enc = tok(prefix_text, return_tensors="pt")
|
|
346
|
+
full_enc = tok(full_text, return_tensors="pt")
|
|
347
|
+
else:
|
|
348
|
+
prefix_enc = tok(prompt, return_tensors="pt")
|
|
349
|
+
full_enc = tok(prompt + " " + target_text, return_tensors="pt")
|
|
350
|
+
|
|
351
|
+
prefix_len = prefix_enc["input_ids"].shape[1]
|
|
352
|
+
labels = full_enc["input_ids"].clone()
|
|
353
|
+
labels[:, :prefix_len] = -100
|
|
354
|
+
enc = dict(full_enc)
|
|
355
|
+
enc["labels"] = labels
|
|
356
|
+
enc.pop("token_type_ids", None) # some VLM processors emit this; generate()/forward() reject it
|
|
357
|
+
return enc
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def run_lora_repair(
|
|
361
|
+
model: "Model",
|
|
362
|
+
train_cases: "CaseBatch | None",
|
|
363
|
+
val_cases: "CaseBatch",
|
|
364
|
+
spec_payload: "dict[str, Any]",
|
|
365
|
+
score_fn: ScoreFn,
|
|
366
|
+
*,
|
|
367
|
+
max_train_examples: int = 16,
|
|
368
|
+
max_steps: int = 30,
|
|
369
|
+
lr: float = 1e-3,
|
|
370
|
+
lora_r: int = 8,
|
|
371
|
+
lora_alpha: int = 16,
|
|
372
|
+
) -> LoraRepairResult:
|
|
373
|
+
"""Train a LoRA adapter on ``train_cases`` (diagnosis-split only — MUST be
|
|
374
|
+
disjoint from ``val_cases``, the batch being validated) and generate on
|
|
375
|
+
``val_cases`` with it applied, so the caller's existing McNemar + e-value
|
|
376
|
+
machinery validates L4 exactly like every other tier — the
|
|
377
|
+
``eval_protocol`` field's "held-out repair effect" half is satisfied by
|
|
378
|
+
that comparison; the "regression battery on baseline-passing cases" half
|
|
379
|
+
is ``n_broken`` on whatever baseline-passing cases the caller's ``data``
|
|
380
|
+
batch includes, which :meth:`~.fix_agent.FixAgent._validate` already
|
|
381
|
+
computes. No second validation path is built here.
|
|
382
|
+
|
|
383
|
+
Executes exactly one recipe shape (``method="lora"``, ``target="llm"``);
|
|
384
|
+
everything else returns ``ok=False`` with a descriptive ``error`` (see
|
|
385
|
+
module docstring). LoRA is spliced into ``model``'s underlying HF module
|
|
386
|
+
*in place* (via ``peft.get_peft_model``) and unloaded again in a
|
|
387
|
+
``finally`` — the model is back to its exact pre-call state (verified by
|
|
388
|
+
this module's tests) whether or not this call ever reaches generation.
|
|
389
|
+
"""
|
|
390
|
+
method = str(spec_payload.get("method", "lora")).strip().lower()
|
|
391
|
+
target = str(spec_payload.get("target", "llm")).strip().lower()
|
|
392
|
+
if method != "lora":
|
|
393
|
+
return LoraRepairResult(
|
|
394
|
+
error=f"only method='lora' is executed; method={method!r} is TODO "
|
|
395
|
+
"(needs a real SFT training loop, not a handful of LoRA steps)"
|
|
396
|
+
)
|
|
397
|
+
if target != "llm":
|
|
398
|
+
return LoraRepairResult(
|
|
399
|
+
error=f"only target='llm' is executed; target={target!r} is TODO "
|
|
400
|
+
"(needs per-architecture module-naming this project has no way to "
|
|
401
|
+
"validate without a training run per architecture)"
|
|
402
|
+
)
|
|
403
|
+
if not lora_available():
|
|
404
|
+
return LoraRepairResult(error="peft is not installed (pip install evalrx[finetune])")
|
|
405
|
+
if train_cases is None or len(train_cases) == 0:
|
|
406
|
+
return LoraRepairResult(
|
|
407
|
+
error="no finetune_pool configured — FixAgent(finetune_pool=...) needs a "
|
|
408
|
+
"diagnosis-only CaseBatch, never the validation split"
|
|
409
|
+
)
|
|
410
|
+
# NOT _resolve_hf(): that helper also requires an image_token_id (it backs
|
|
411
|
+
# the vision-only visual_embedding_boost primitive) and would reject any
|
|
412
|
+
# text-only backbone -- exactly the fallback target_modules case below.
|
|
413
|
+
hf = getattr(model, "_hf", None)
|
|
414
|
+
hf_model = hf[0] if isinstance(hf, tuple) and len(hf) == 2 else None
|
|
415
|
+
processor = hf[1] if isinstance(hf, tuple) and len(hf) == 2 else None
|
|
416
|
+
if hf_model is None or processor is None:
|
|
417
|
+
return LoraRepairResult(
|
|
418
|
+
error="backend internals unavailable (needs a loaded hf_local model + processor)"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
examples = _build_sft_examples(train_cases, max_train_examples)
|
|
422
|
+
if not examples:
|
|
423
|
+
return LoraRepairResult(
|
|
424
|
+
error="finetune_pool has no failing case with a known gold answer to train on"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
import torch
|
|
428
|
+
from peft import LoraConfig, TaskType, get_peft_model
|
|
429
|
+
|
|
430
|
+
chat_template_kwargs = dict(getattr(getattr(model, "spec", None), "chat_template_kwargs", {}) or {})
|
|
431
|
+
target_modules = _lora_target_modules(hf_model)
|
|
432
|
+
lora_cfg = LoraConfig(
|
|
433
|
+
r=lora_r, lora_alpha=lora_alpha, lora_dropout=0.0,
|
|
434
|
+
target_modules=target_modules, task_type=TaskType.CAUSAL_LM,
|
|
435
|
+
)
|
|
436
|
+
was_training = hf_model.training
|
|
437
|
+
device = next(hf_model.parameters()).device
|
|
438
|
+
# get_peft_model() itself raises (before injecting anything, so hf_model
|
|
439
|
+
# is untouched) when target_modules matches zero layers -- e.g. a VLM
|
|
440
|
+
# whose language_model uses non-LLaMA-style projection names. That must
|
|
441
|
+
# not propagate as an uncaught exception out of one candidate's
|
|
442
|
+
# validation and abort the whole FixAgent run.
|
|
443
|
+
try:
|
|
444
|
+
peft_model = get_peft_model(hf_model, lora_cfg)
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
return LoraRepairResult(
|
|
447
|
+
error=f"no matching linear layers for target_modules={target_modules!r} "
|
|
448
|
+
f"on this architecture: {exc}"
|
|
449
|
+
)
|
|
450
|
+
try:
|
|
451
|
+
trainable = [p for p in peft_model.parameters() if p.requires_grad]
|
|
452
|
+
if not trainable: # defensive belt-and-braces; get_peft_model already guards this
|
|
453
|
+
return LoraRepairResult(
|
|
454
|
+
error=f"no matching linear layers for target_modules={target_modules!r} "
|
|
455
|
+
"on this architecture"
|
|
456
|
+
)
|
|
457
|
+
peft_model.train()
|
|
458
|
+
opt = torch.optim.AdamW(trainable, lr=lr)
|
|
459
|
+
n_steps_run = 0
|
|
460
|
+
for step in range(max_steps):
|
|
461
|
+
case, target_text = examples[step % len(examples)]
|
|
462
|
+
try:
|
|
463
|
+
enc = _encode_sft_example(
|
|
464
|
+
processor, str(case.inputs.prompt or ""), getattr(case.inputs, "image", None),
|
|
465
|
+
target_text, chat_template_kwargs,
|
|
466
|
+
)
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
logger.debug("run_lora_repair: encode failed on %s: %s", case.id, exc)
|
|
469
|
+
continue
|
|
470
|
+
enc = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in enc.items()}
|
|
471
|
+
opt.zero_grad()
|
|
472
|
+
try:
|
|
473
|
+
out = peft_model(**enc)
|
|
474
|
+
if out.loss is None:
|
|
475
|
+
continue
|
|
476
|
+
out.loss.backward()
|
|
477
|
+
opt.step()
|
|
478
|
+
n_steps_run += 1
|
|
479
|
+
except Exception as exc:
|
|
480
|
+
logger.debug("run_lora_repair: training step failed on %s: %s", case.id, exc)
|
|
481
|
+
continue
|
|
482
|
+
if n_steps_run == 0:
|
|
483
|
+
return LoraRepairResult(error="every training step failed to encode/run (see debug log)")
|
|
484
|
+
|
|
485
|
+
peft_model.eval()
|
|
486
|
+
scores: "dict[str, Optional[bool]]" = {}
|
|
487
|
+
with torch.no_grad():
|
|
488
|
+
for case in val_cases:
|
|
489
|
+
try:
|
|
490
|
+
out = str(model.generate(case.inputs))
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
logger.debug("run_lora_repair: generate failed on %s: %s", case.id, exc)
|
|
493
|
+
scores[case.id] = None
|
|
494
|
+
continue
|
|
495
|
+
scores[case.id] = score_to_bool(score_fn(case, out))
|
|
496
|
+
return LoraRepairResult(scores=scores, ok=True)
|
|
497
|
+
finally:
|
|
498
|
+
peft_model.unload()
|
|
499
|
+
hf_model.train(was_training)
|