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.
Files changed (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. evalrx-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,204 @@
1
+ """TruthX-backed IFCD components.
2
+
3
+ IFCD's paper contrasts an anti-hallucination and a hallucination-inducing
4
+ internal-representation edit. This module ports the released TruthX MLP
5
+ editor and exposes it through reversible modern-HF hooks. The hooks edit the
6
+ post-projection attention output (the current Transformers boundary), whereas
7
+ the original LLaMA patch edits just before ``o_proj``; callers must therefore
8
+ report this route as an architecture adaptation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Iterator
14
+ from contextlib import contextmanager
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ def _dims(value: Any) -> list[int]:
20
+ text = str(value or "")
21
+ return [int(item) for item in text.split(",") if item]
22
+
23
+
24
+ def _checkpoint_arg(args: Any, name: str) -> Any:
25
+ return args.get(name) if isinstance(args, dict) else getattr(args, name)
26
+
27
+
28
+ class TruthXEditor:
29
+ """Released TruthX autoencoder with a reversible edit-strength switch."""
30
+
31
+ def __init__(self, checkpoint_path: str | Path, *, hidden_size: int, top_layers: int = 15):
32
+ import torch
33
+ from torch import nn
34
+
35
+ path = Path(checkpoint_path)
36
+ if not path.is_file():
37
+ raise FileNotFoundError(f"TruthX checkpoint does not exist: {path}")
38
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
39
+ checkpoint = torch.load(path, map_location=device, weights_only=False)
40
+ args = checkpoint["args"]
41
+ semantic_dim = int(_checkpoint_arg(args, "semantic_latent_dim"))
42
+ truthful_dim = int(_checkpoint_arg(args, "truthful_latent_dim"))
43
+
44
+ def stack(in_dim: int, hidden: list[int], out_dim: int, *, normalize: bool) -> Any:
45
+ modules: list[Any] = []
46
+ current = in_dim
47
+ for dim in hidden:
48
+ modules.append(nn.Sequential(nn.Linear(current, dim), nn.LayerNorm(dim), nn.LeakyReLU()))
49
+ current = dim
50
+ modules.append(nn.Sequential(nn.Linear(current, out_dim), nn.LayerNorm(out_dim), nn.LeakyReLU()))
51
+ return nn.Sequential(*modules)
52
+
53
+ class Autoencoder(nn.Module):
54
+ def __init__(self) -> None:
55
+ super().__init__()
56
+ self.semantic_encoder = stack(
57
+ hidden_size, _dims(_checkpoint_arg(args, "semantic_hidden_dims")), semantic_dim, normalize=False
58
+ )
59
+ self.truthful_encoder = stack(
60
+ hidden_size, _dims(_checkpoint_arg(args, "truthful_hidden_dims")), truthful_dim, normalize=True
61
+ )
62
+ self.proj = nn.Linear(truthful_dim, semantic_dim, bias=False) if semantic_dim != truthful_dim else None
63
+ decoder_dims = _dims(_checkpoint_arg(args, "decoder_hidden_dims"))
64
+ decoder: list[Any] = []
65
+ current = semantic_dim
66
+ for dim in decoder_dims:
67
+ decoder.append(nn.Sequential(nn.Linear(current, dim), nn.LayerNorm(dim), nn.LeakyReLU()))
68
+ current = dim
69
+ self.decoder = nn.Sequential(*decoder) if decoder else nn.Identity()
70
+ # TruthX stores this as ``nn.Sequential(Linear(...))``;
71
+ # preserve the state-dict key ``final_layer.0`` exactly.
72
+ self.final_layer = nn.Sequential(nn.Linear(current, hidden_size))
73
+ self.cross_attention = nn.MultiheadAttention(embed_dim=semantic_dim, num_heads=1)
74
+
75
+ def truth(self, x: Any) -> Any:
76
+ return torch.nn.functional.normalize(self.truthful_encoder(x), p=2, dim=-1)
77
+
78
+ def reconstruct(self, x: Any, truth: Any) -> Any:
79
+ semantic = self.semantic_encoder(x)
80
+ if self.proj is not None:
81
+ truth = self.proj(truth)
82
+ attended, _ = self.cross_attention(
83
+ semantic.unsqueeze(0), truth.unsqueeze(0), truth.unsqueeze(0)
84
+ )
85
+ return self.final_layer(self.decoder(semantic + attended[0]))
86
+
87
+ self.model = Autoencoder().to(device)
88
+ self.model.load_state_dict(checkpoint["state_dict"])
89
+ self.model.eval()
90
+ self.pos_center = checkpoint["pos_center"].to(device)
91
+ self.neg_center = checkpoint["neg_center"].to(device)
92
+ # The official release serializes this as a Python list, unlike the
93
+ # center tensors. Keep it host-side because it only gates hook setup.
94
+ self.rank = [int(value) for value in checkpoint["rank"]]
95
+ self.top_layers = int(top_layers)
96
+ self.strength = 0.5
97
+
98
+ def edit(self, values: Any, *, layer_index: int, component: str) -> Any:
99
+ """Apply TruthX's positive or negative edit only to the final token."""
100
+ import torch
101
+ import torch.nn.functional as F
102
+
103
+ rank_index = 2 * int(layer_index) + (0 if component == "attn" else 1)
104
+ if rank_index >= len(self.rank) or int(self.rank[rank_index]) > self.top_layers:
105
+ return values
106
+ batch, sequence, width = values.shape
107
+ flat = values.contiguous().view(-1, width)
108
+ dtype = self.model.semantic_encoder[0][0].weight.dtype
109
+ x = flat.to(dtype)
110
+ # TruthX encodes the unflattened sequence, then flattens it inside its
111
+ # autoencoder forward. Keeping that order is material: its
112
+ # cross-attention is over tokens in the current generated sequence.
113
+ truth = self.model.truth(values.to(dtype))
114
+ pos = self.pos_center[rank_index].unsqueeze(0).to(dtype)
115
+ neg = self.neg_center[rank_index].unsqueeze(0).to(dtype)
116
+ direction = (pos - neg).unsqueeze(0)
117
+ positive = self.model.reconstruct(
118
+ x, F.normalize(truth + direction, p=2, dim=-1).reshape(-1, truth.shape[-1])
119
+ )
120
+ negative = self.model.reconstruct(
121
+ x, F.normalize(truth - direction, p=2, dim=-1).reshape(-1, truth.shape[-1])
122
+ )
123
+ delta = (positive - negative).view(batch, sequence, width).to(values.dtype)
124
+ delta = F.normalize(delta, p=2, dim=-1) * torch.linalg.vector_norm(
125
+ values, ord=2, dim=-1, keepdim=True
126
+ )
127
+ mask = torch.zeros((batch, sequence, 1), device=values.device, dtype=values.dtype)
128
+ mask[:, -1:, :] = 1
129
+ return values + delta.to(values.dtype) * float(self.strength) * mask
130
+
131
+
132
+ @contextmanager
133
+ def truthx_editing(language_model: Any, editor: TruthXEditor) -> Iterator[None]:
134
+ """Install temporary TruthX hooks on the LLaMA attention and MLP outputs."""
135
+ decoder = getattr(language_model, "model", language_model)
136
+ handles: list[Any] = []
137
+
138
+ def edit_output(layer_index: int, component: str):
139
+ def hook(_module: Any, _inputs: Any, output: Any) -> Any:
140
+ if isinstance(output, tuple):
141
+ return (editor.edit(output[0], layer_index=layer_index, component=component), *output[1:])
142
+ return editor.edit(output, layer_index=layer_index, component=component)
143
+
144
+ return hook
145
+
146
+ try:
147
+ for index, layer in enumerate(decoder.layers):
148
+ handles.append(layer.self_attn.register_forward_hook(edit_output(index, "attn")))
149
+ handles.append(layer.mlp.register_forward_hook(edit_output(index, "ffn")))
150
+ yield
151
+ finally:
152
+ for handle in handles:
153
+ handle.remove()
154
+
155
+
156
+ class IFCDLogitsProcessor:
157
+ """Compute ``(1 + alpha) log p+ - alpha log p-`` with IFCD's cutoff."""
158
+
159
+ def __init__(
160
+ self,
161
+ model: Any,
162
+ negative_inputs: dict[str, Any],
163
+ editor: TruthXEditor,
164
+ *,
165
+ alpha: float = 0.1,
166
+ beta: float = 0.1,
167
+ edit_strength: float = 0.5,
168
+ ) -> None:
169
+ self.model = model
170
+ self.negative_inputs = negative_inputs
171
+ self.editor = editor
172
+ self.alpha = float(alpha)
173
+ self.beta = float(beta)
174
+ self.edit_strength = float(edit_strength)
175
+ self._negative_output: Any = None
176
+
177
+ def __call__(self, input_ids: Any, scores: Any) -> Any:
178
+ import math
179
+
180
+ import torch
181
+ import torch.nn.functional as F
182
+
183
+ positive = F.log_softmax(scores, dim=-1)
184
+ old_strength = self.editor.strength
185
+ self.editor.strength = -self.edit_strength
186
+ try:
187
+ with torch.no_grad():
188
+ if self._negative_output is None:
189
+ self._negative_output = self.model(
190
+ **self.negative_inputs, use_cache=True, return_dict=True
191
+ )
192
+ else:
193
+ self._negative_output = self.model(
194
+ input_ids=input_ids[:, -1:],
195
+ past_key_values=self._negative_output.past_key_values,
196
+ use_cache=True,
197
+ return_dict=True,
198
+ )
199
+ finally:
200
+ self.editor.strength = old_strength
201
+ negative = F.log_softmax(self._negative_output.logits[:, -1, :], dim=-1)
202
+ cutoff = positive.max(dim=-1, keepdim=True).values + math.log(self.beta)
203
+ contrastive = (1 + self.alpha) * positive - self.alpha * negative
204
+ return contrastive.masked_fill(positive < cutoff, -float("inf"))
@@ -0,0 +1,164 @@
1
+ """PAI decoding executor from *Paying More Attention to Image*.
2
+
3
+ PAI's released LLaVA route increases the pre-softmax attention score from the
4
+ currently generated token to image tokens in a contiguous layer interval. The
5
+ paired classifier-free-guidance branch uses a separate image-free KV cache.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import types
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+ from typing import Any
14
+
15
+
16
+ @contextmanager
17
+ def image_attention_boost(
18
+ model: Any,
19
+ *,
20
+ image_start: int,
21
+ image_end: int,
22
+ alpha: float = 0.2,
23
+ start_layer: int = 2,
24
+ end_layer: int = 32,
25
+ ) -> Iterator[None]:
26
+ """Temporarily apply PAI's released pre-softmax image-attention formula.
27
+
28
+ ``image_end`` is exclusive. The patch targets only LLaMA eager attention;
29
+ callers must restore it after generation so a candidate cannot leak into a
30
+ later baseline or a different repair attempt.
31
+ """
32
+ import torch
33
+ import torch.nn.functional as F
34
+ from transformers.models.llama.modeling_llama import apply_rotary_pos_emb, repeat_kv
35
+
36
+ decoder = getattr(model, "model", model)
37
+ all_layers = decoder.layers
38
+ layers = all_layers[start_layer : min(end_layer, len(all_layers))]
39
+ original_forwards = [layer.self_attn.forward for layer in layers]
40
+
41
+ def pai_forward(
42
+ attention: Any,
43
+ hidden_states: Any,
44
+ position_embeddings: tuple[Any, Any],
45
+ attention_mask: Any = None,
46
+ past_key_values: Any = None,
47
+ cache_position: Any = None,
48
+ **kwargs: Any,
49
+ ) -> tuple[Any, Any]:
50
+ input_shape = hidden_states.shape[:-1]
51
+ hidden_shape = (*input_shape, -1, attention.head_dim)
52
+ query_states = attention.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
53
+ key_states = attention.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
54
+ value_states = attention.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
55
+ cos, sin = position_embeddings
56
+ query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
57
+ if past_key_values is not None:
58
+ cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
59
+ key_states, value_states = past_key_values.update(
60
+ key_states, value_states, attention.layer_idx, cache_kwargs
61
+ )
62
+ keys = repeat_kv(key_states, attention.num_key_value_groups)
63
+ values = repeat_kv(value_states, attention.num_key_value_groups)
64
+ weights = torch.matmul(query_states, keys.transpose(2, 3)) * attention.scaling
65
+ if attention_mask is not None:
66
+ weights = weights + attention_mask[:, :, :, : keys.shape[-2]]
67
+ # PAI (attention.py): a <- a + alpha * abs(a), at the final query
68
+ # position and the image token interval, before softmax. Its CFG
69
+ # processor makes an image-free language-model call using this same
70
+ # temporary patch; source PAI disables the boost for that call.
71
+ stop = min(int(image_end), weights.shape[-1])
72
+ if not getattr(attention, "_evalrx_pai_suspend", False) and int(image_start) < stop:
73
+ target = weights[:, :, -1, int(image_start) : stop]
74
+ weights[:, :, -1, int(image_start) : stop] = target + target.abs() * float(alpha)
75
+ weights = F.softmax(weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
76
+ weights = F.dropout(
77
+ weights,
78
+ p=0.0 if not attention.training else attention.attention_dropout,
79
+ training=attention.training,
80
+ )
81
+ output = torch.matmul(weights, values).transpose(1, 2).contiguous()
82
+ output = output.reshape(*input_shape, -1).contiguous()
83
+ output = attention.o_proj(output)
84
+ return output, weights if kwargs.get("output_attentions", False) else None
85
+
86
+ try:
87
+ for layer in layers:
88
+ layer.self_attn.forward = types.MethodType(pai_forward, layer.self_attn)
89
+ yield
90
+ finally:
91
+ for layer, original in zip(layers, original_forwards):
92
+ layer.self_attn.forward = original
93
+
94
+
95
+ class PAICFGLogitsProcessor:
96
+ """Released PAI classifier-free-guidance decoding for a LLaMA LM.
97
+
98
+ PAI derives an unconditional cache from the same prompt with the image
99
+ token span removed. It combines conditional and unconditional log
100
+ probabilities at every generation step, while temporarily suppressing the
101
+ image-attention boost in the unconditional pass. This mirrors
102
+ ``CFG.py`` in the authors' release, without persisting a second model.
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ model: Any,
108
+ unconditional_input_ids: Any,
109
+ *,
110
+ guidance_scale: float = 2.0,
111
+ start_layer: int = 2,
112
+ end_layer: int = 32,
113
+ ) -> None:
114
+ # HF LLaVA stores a bare ``LlamaModel`` in ``language_model`` and the
115
+ # final LM head on the outer wrapper. Use the wrapper for CFG logits,
116
+ # but locate the inner decoder layers to suspend PAI's attention boost.
117
+ self.model = model
118
+ self.unconditional_input_ids = unconditional_input_ids
119
+ self.guidance_scale = float(guidance_scale)
120
+ language_model = getattr(model, "language_model", model)
121
+ decoder = getattr(language_model, "model", language_model)
122
+ self.layers = decoder.layers[start_layer : min(end_layer, len(decoder.layers))]
123
+ self._output: Any = None
124
+
125
+ @contextmanager
126
+ def _without_attention_boost(self) -> Iterator[None]:
127
+ previous = [
128
+ getattr(layer.self_attn, "_evalrx_pai_suspend", False) for layer in self.layers
129
+ ]
130
+ try:
131
+ for layer in self.layers:
132
+ layer.self_attn._evalrx_pai_suspend = True
133
+ yield
134
+ finally:
135
+ for layer, value in zip(self.layers, previous):
136
+ layer.self_attn._evalrx_pai_suspend = value
137
+
138
+ def __call__(self, input_ids: Any, scores: Any) -> Any:
139
+ import math
140
+
141
+ import torch
142
+ import torch.nn.functional as F
143
+
144
+ conditional = F.log_softmax(scores, dim=-1)
145
+ if self.guidance_scale == 1.0:
146
+ return conditional
147
+ with self._without_attention_boost(), torch.no_grad():
148
+ if self._output is None:
149
+ self._output = self.model(
150
+ input_ids=self.unconditional_input_ids,
151
+ use_cache=True,
152
+ return_dict=True,
153
+ )
154
+ else:
155
+ self._output = self.model(
156
+ input_ids=input_ids[:, -1:],
157
+ use_cache=True,
158
+ past_key_values=self._output.past_key_values,
159
+ return_dict=True,
160
+ )
161
+ unconditional = F.log_softmax(self._output.logits[:, -1, :], dim=-1)
162
+ cutoff = conditional.max(dim=-1, keepdim=True).values + math.log(0.1)
163
+ guided = self.guidance_scale * (conditional - unconditional) + unconditional
164
+ return guided.masked_fill(conditional < cutoff, -float("inf"))
@@ -0,0 +1,202 @@
1
+ """Temporal Contrastive Decoding (TCD) — Li et al. 2026, arXiv:2604.15383.
2
+
3
+ TCD is NOT a VCD-family port. VCD/IFCD/PAI all share one combine rule
4
+ (``(1+alpha)*z - alpha*z_tilde`` masked below a ``max + log(beta)`` cutoff,
5
+ see :mod:`evalrx.models.paper_methods.vcd`); TCD's is additive,
6
+ positive-rectified, and restricted to a small candidate set, with no cutoff
7
+ masking at all (Eq. 7-9 below). The two other structural differences from
8
+ VCD's per-step-noisy-forward pattern:
9
+
10
+ * the "corrupted" view is a **waveform** operation (Hann-window blur, Eq. 1)
11
+ followed by a full audio-encoder re-encode, computed ONCE before decoding
12
+ starts — not a per-step latent perturbation;
13
+ * the blur window and update scale are **per-example adaptive**, derived from
14
+ a self-normalized stability score (Eq. 2-6) that needs the audio encoder's
15
+ per-layer hidden-state trajectory AND the decoder's per-layer attention to
16
+ audio tokens — signals VCD/IFCD/PAI never touch.
17
+
18
+ This module holds pure, framework-light math only (all Eq. references are to
19
+ the paper's Section 3 / Appendix A). The orchestration — encoding, KV-cache
20
+ management, the two-branch decode loop — lives in
21
+ ``HFLocalModel.generate_tcd`` (``models/backends/hf_local.py``), matching
22
+ this package's split of "paper math here, model plumbing in the backend".
23
+
24
+ Hyperparameter defaults (:class:`TCDHyperparams`) are Table 6 verbatim, read
25
+ directly from the PDF (Appendix A) — not re-derived, not taken from a
26
+ WebFetch summary of the paper.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import math
32
+ from dataclasses import dataclass
33
+ from typing import Any
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class TCDHyperparams:
38
+ """Table 6 defaults. The paper uses ONE configuration across benchmarks;
39
+ only ``gamma_gate`` is set per backbone (2.0 for Qwen2-Audio-Instruct,
40
+ the paper's own hyperparameter anchor — see Appendix A.1/A.2(4))."""
41
+
42
+ l_attn: int = 4 # last decoder layers aggregated for the step-wise gate's r_t (Eq. 8)
43
+ tau: float = 4.0 # stability aggregation softmax temperature (Eq. 4)
44
+ w_min: float = 8.0 # blur window bounds, ms (Eq. 5)
45
+ w_max: float = 30.0
46
+ lam_min: float = 0.3 # update-scale bounds (Eq. 6)
47
+ lam_max: float = 1.5
48
+ k_orig: int = 16 # candidate set from the original logits (Eq. 9's Omega_t)
49
+ k_blur: int = 8 # candidate set from the slow-path logits
50
+ gamma_gate: float = 2.0 # gate multiplier; paper value for Qwen2-Audio-Instruct
51
+ alpha_entropy: float = 0.5 # entropy exponent in the gate (Eq. 8)
52
+ k_ent: int = 5 # top-K for the normalized entropy signal (Eq. 8)
53
+ eps: float = 1e-6 # numerical stability term (Eq. 3)
54
+
55
+
56
+ def hann_blur_waveform(waveform: Any, sample_rate: int, window_ms: float) -> Any:
57
+ """Eq. 1: temporally blur the waveform with a normalized Hann window, then
58
+ rescale to preserve global RMS amplitude ("rescaling x_tilde to preserve
59
+ global amplitude", Section 3.2).
60
+
61
+ ``waveform`` is a 1-D mono float32 numpy array (matches
62
+ ``hf_local._resolve_audio``'s contract). Returns the blurred waveform at
63
+ the same length and sample rate — it is re-encoded through the SAME
64
+ processor path as the original audio, not treated specially downstream.
65
+ """
66
+ import numpy as np
67
+
68
+ win_len = max(1, int(round(float(window_ms) / 1000.0 * sample_rate)))
69
+ if win_len % 2 == 0:
70
+ win_len += 1 # odd length -> symmetric window, unambiguous center tap
71
+ if win_len <= 1:
72
+ return np.asarray(waveform, dtype=np.float32).copy()
73
+
74
+ window = np.hanning(win_len).astype(np.float32)
75
+ window_sum = window.sum()
76
+ if window_sum <= 0: # pragma: no cover - hanning(n>=3) is always positive-summed
77
+ return np.asarray(waveform, dtype=np.float32).copy()
78
+ window = window / window_sum
79
+
80
+ pad = win_len // 2
81
+ padded = np.pad(np.asarray(waveform, dtype=np.float32), (pad, pad), mode="reflect")
82
+ blurred = np.convolve(padded, window, mode="valid").astype(np.float32)
83
+
84
+ orig_rms = float(np.sqrt(np.mean(np.square(waveform)))) + 1e-8
85
+ blur_rms = float(np.sqrt(np.mean(np.square(blurred)))) + 1e-8
86
+ return (blurred * (orig_rms / blur_rms)).astype(np.float32)
87
+
88
+
89
+ def layer_stability(hidden_states: list, *, eps: float) -> Any:
90
+ """Eq. 2-3: per-layer magnitude ``M``, temporal flux ``F``, and stability
91
+ ``S_l = M_l / (M_l + F_l + eps)``.
92
+
93
+ ``hidden_states`` is a list of per-layer encoder hidden-state tensors,
94
+ each ``(seq_len, dim)`` with the batch dimension already stripped (one
95
+ entry per encoder layer, time-ordered along ``seq_len`` — the audio
96
+ encoder's own frame axis, NOT decoding steps). Returns a 1-D tensor of
97
+ per-layer stability scores, same length as ``hidden_states``.
98
+ """
99
+ import torch
100
+
101
+ magnitudes, fluxes = [], []
102
+ for h in hidden_states:
103
+ h = h.float()
104
+ magnitudes.append(h.norm(dim=-1).mean())
105
+ if h.shape[0] > 1:
106
+ fluxes.append((h[1:] - h[:-1]).norm(dim=-1).mean())
107
+ else: # pragma: no cover - a single-frame encoder trajectory is degenerate input
108
+ fluxes.append(torch.zeros((), dtype=h.dtype, device=h.device))
109
+ M = torch.stack(magnitudes)
110
+ F = torch.stack(fluxes)
111
+ return M / (M + F + eps)
112
+
113
+
114
+ def audio_attention_ratio(attn_layer: Any, audio_mask: Any) -> Any:
115
+ """Fraction of one decoder layer's attention mass landing on audio-token
116
+ key positions, averaged over heads and query rows.
117
+
118
+ ``attn_layer`` is ``(heads, q, k)`` (batch stripped); ``audio_mask`` is a
119
+ bool tensor ``(k,)``. Shared by both Eq. 4's per-layer ``r_l`` (all
120
+ layers, computed once from the prefill) and Eq. 8's step-wise ``r_t``
121
+ (last ``l_attn`` layers, recomputed at every decode step) — same
122
+ quantity, different layer subset and call frequency.
123
+ """
124
+ attn_layer = attn_layer.float()
125
+ audio_mass = attn_layer[..., audio_mask].sum(dim=-1)
126
+ total_mass = attn_layer.sum(dim=-1).clamp_min(1e-8)
127
+ return (audio_mass / total_mass).mean()
128
+
129
+
130
+ def aggregate_stability(stability_per_layer: Any, attention_ratio_per_layer: Any, *, temperature: float) -> float:
131
+ """Eq. 4: softmax-weight per-layer stability by audio-attention ratio.
132
+
133
+ Both inputs are 1-D tensors of equal length. This equal-length
134
+ requirement is exactly the point in the paper where per-layer stability
135
+ (an ENCODER-side quantity) and per-layer attention ratio (a DECODER-side
136
+ quantity) get zipped together index-for-index — faithful only when the
137
+ audio encoder and text decoder have the same layer count (true for
138
+ Qwen2-Audio-Instruct: 32 encoder / 32 decoder layers; NOT true for e.g.
139
+ Qwen2.5-Omni's 32/28 split). Callers must truncate both inputs to
140
+ ``min(len(...), len(...))`` themselves when depths differ, and report
141
+ ``paper_method_fidelity("tcd")`` as ``"adapted"`` in that case.
142
+ """
143
+ import torch
144
+
145
+ if stability_per_layer.shape[0] != attention_ratio_per_layer.shape[0]:
146
+ raise ValueError(
147
+ "aggregate_stability requires equal-length per-layer inputs "
148
+ f"(got {stability_per_layer.shape[0]} stability vs "
149
+ f"{attention_ratio_per_layer.shape[0]} attention-ratio entries); "
150
+ "truncate to min(...) before calling for a mismatched-depth architecture"
151
+ )
152
+ weights = torch.softmax(temperature * attention_ratio_per_layer.float(), dim=0)
153
+ return float((weights * stability_per_layer.float()).sum())
154
+
155
+
156
+ def adaptive_blur_params(stability: float, hp: TCDHyperparams) -> "tuple[float, float]":
157
+ """Eq. 5-6: map the scalar stability score to (blur window ms, update scale)."""
158
+ window_ms = hp.w_min + (hp.w_max - hp.w_min) * stability
159
+ lam = hp.lam_min + (hp.lam_max - hp.lam_min) * stability
160
+ return float(window_ms), float(lam)
161
+
162
+
163
+ def topk_renormalized_entropy(logits: Any, k: int) -> float:
164
+ """Eq. 8's uncertainty signal: normalized entropy of softmax(logits),
165
+ RENORMALIZED over its own top-K probability mass first, then divided by
166
+ log(K) so the result is bounded in [0, 1]."""
167
+ import torch
168
+
169
+ probs = torch.softmax(logits.float(), dim=-1)
170
+ top = torch.topk(probs, min(int(k), probs.shape[-1]))
171
+ top_probs = top.values / top.values.sum().clamp_min(1e-12)
172
+ entropy = -(top_probs * top_probs.clamp_min(1e-12).log()).sum()
173
+ denom = math.log(top.values.shape[-1]) if top.values.shape[-1] > 1 else 1.0
174
+ return float(entropy / denom)
175
+
176
+
177
+ def reliance_gate(audio_reliance: float, entropy_hat: float, hp: TCDHyperparams) -> float:
178
+ """Eq. 8: ``g_t = min(gamma_gate * r_t * H_hat_t ** alpha, 1.0)``."""
179
+ return min(hp.gamma_gate * float(audio_reliance) * (max(entropy_hat, 0.0) ** hp.alpha_entropy), 1.0)
180
+
181
+
182
+ def fuse_logits(z: Any, z_tilde: Any, *, lam: float, gate_value: float, hp: TCDHyperparams) -> Any:
183
+ """Eq. 7-9: gated, candidate-restricted, positive-only logit update.
184
+
185
+ Unlike VCD/IFCD/PAI, there is NO cutoff masking here — tokens outside the
186
+ candidate set ``Omega_t`` are returned UNCHANGED (``z_t(j)`` verbatim),
187
+ they are not suppressed to ``-inf``. Only the union of each branch's
188
+ top-K is nudged, and only upward (``ReLU(z - z_tilde)``, Eq. 7), scaled
189
+ by ``lam * gate_value``.
190
+ """
191
+ import torch
192
+
193
+ positive_diff = (z - z_tilde).clamp_min(0.0)
194
+ orig_idx = torch.topk(z, min(hp.k_orig, z.shape[-1])).indices
195
+ blur_idx = torch.topk(z_tilde, min(hp.k_blur, z_tilde.shape[-1])).indices
196
+ candidates = torch.zeros_like(z, dtype=torch.bool)
197
+ candidates[orig_idx] = True
198
+ candidates[blur_idx] = True
199
+
200
+ fused = z.clone()
201
+ fused[candidates] = z[candidates] + lam * gate_value * positive_diff[candidates]
202
+ return fused
@@ -0,0 +1,45 @@
1
+ """Hugging Face adapter for VCD's released per-token contrastive sampler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+
9
+ class VCDLogitsProcessor:
10
+ """Contrast clean scores with a cached noisy-image generation path.
11
+
12
+ The authors' sampling loop performs the second multimodal forward at every
13
+ decode step. This processor owns the corresponding noisy KV cache, which
14
+ lets an unmodified Hugging Face LLaVA ``generate`` call execute the same
15
+ score formula.
16
+ """
17
+
18
+ def __init__(self, model: Any, noisy_inputs: dict[str, Any], *, alpha: float, beta: float) -> None:
19
+ self.model = model
20
+ self.noisy_inputs = noisy_inputs
21
+ self.alpha = float(alpha)
22
+ self.beta = float(beta)
23
+ self._noisy_output: Any = None
24
+
25
+ def __call__(self, input_ids: Any, scores: Any) -> Any:
26
+ import torch
27
+
28
+ with torch.no_grad():
29
+ if self._noisy_output is None:
30
+ self._noisy_output = self.model(
31
+ **self.noisy_inputs,
32
+ use_cache=True,
33
+ return_dict=True,
34
+ )
35
+ else:
36
+ self._noisy_output = self.model(
37
+ input_ids=input_ids[:, -1:],
38
+ past_key_values=self._noisy_output.past_key_values,
39
+ use_cache=True,
40
+ return_dict=True,
41
+ )
42
+ noisy_scores = self._noisy_output.logits[:, -1, :]
43
+ cutoff = scores.max(dim=-1, keepdim=True).values + math.log(self.beta)
44
+ contrastive = (1.0 + self.alpha) * scores - self.alpha * noisy_scores
45
+ return contrastive.masked_fill(scores < cutoff, -float("inf"))