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,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"))
|