v1.0: demo montada desde el tag v1.0 del repositorio
Browse files- README.md +25 -5
- app.py +642 -0
- artifacts/demo/sector_vo_pythia.json +23 -0
- artifacts/demo/sector_vo_pythia.safetensors +3 -0
- artifacts/demo/sector_vo_vitb.json +23 -0
- artifacts/demo/sector_vo_vitb.safetensors +3 -0
- artifacts/tables/validacion_e2e.csv +8 -0
- demo/portador.py +119 -0
- requirements.txt +8 -0
- src/__init__.py +0 -0
- src/config.py +79 -0
- src/datos.py +148 -0
- src/gauges.py +207 -0
- src/metrics.py +42 -0
- src/quantizer.py +42 -0
- src/reconstruccion.py +136 -0
- src/weights.py +241 -0
README.md
CHANGED
|
@@ -1,13 +1,33 @@
|
|
| 1 |
---
|
| 2 |
-
title: Gauge
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Gauge equivalence does not survive quantisation
|
| 3 |
+
emoji: "\U0001FA9E"
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.22.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Gauge equivalence does not survive quantisation
|
| 14 |
+
|
| 15 |
+
Interactive companion to the note *Gauge equivalence does not survive
|
| 16 |
+
quantisation: value-output orbit, norm-product bound, and
|
| 17 |
+
consequences for rotational methods*.
|
| 18 |
+
|
| 19 |
+
Pick a point of the value-output gauge orbit and see what
|
| 20 |
+
quantisation does to the OV circuit. The gauge leaves the circuit
|
| 21 |
+
exactly fixed in real arithmetic; the demo shows the error staying at
|
| 22 |
+
the fp64 floor before quantising and exploding after it.
|
| 23 |
+
|
| 24 |
+
Nothing is reimplemented here: the app runs the note's own
|
| 25 |
+
`src/gauges.py`, `src/quantizer.py` and `src/metrics.py` on a light
|
| 26 |
+
carrier of the value-output sector. `scripts/paridad_demo.py`
|
| 27 |
+
certifies the demo cell by cell against the published sweeps.
|
| 28 |
+
|
| 29 |
+
The parity gate is not decoration: it replays the published sweeps'
|
| 30 |
+
own gauges and requires the demo to return the same numbers to
|
| 31 |
+
$3\times10^{-15}$, against a tolerance of $10^{-12}$.
|
| 32 |
+
|
| 33 |
+
Code and paper: https://github.com/mmunozpl/gauge-orbit-quantization
|
app.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""demo interactiva: la órbita de gauge valor-salida bajo cuantización.
|
| 2 |
+
|
| 3 |
+
tres pestañas. la órbita, donde se elige un punto y se ve qué le hace
|
| 4 |
+
la cuantización al circuito; el interruptor, con la validación
|
| 5 |
+
end-to-end ya medida; y qué se mide, con el método.
|
| 6 |
+
|
| 7 |
+
toda la aritmética la ejecuta el código del paper ---`src/gauges.py`,
|
| 8 |
+
`src/quantizer.py`, `src/metrics.py`--- sobre el portador ligero de
|
| 9 |
+
`demo/portador.py`. aquí no se reimplementa nada, y por eso
|
| 10 |
+
`scripts/paridad_demo.py` puede certificar la demo celda a celda
|
| 11 |
+
contra los csv publicados.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import gradio as gr
|
| 18 |
+
import matplotlib
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
matplotlib.use("Agg") # sin servidor gráfico en el Space
|
| 22 |
+
|
| 23 |
+
from matplotlib.figure import Figure # noqa: E402
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _raiz() -> Path:
|
| 27 |
+
"""la raíz del árbol, igual en el repo que en el Space.
|
| 28 |
+
|
| 29 |
+
en el repositorio este fichero vive en `demo/`; en el Space, la
|
| 30 |
+
app viaja a la raíz para que Gradio la encuentre. se sube hasta
|
| 31 |
+
dar con el directorio que contiene `src` y `demo`, y así el mismo
|
| 32 |
+
código sirve en los dos sitios sin condicionales.
|
| 33 |
+
|
| 34 |
+
returns:
|
| 35 |
+
el directorio raíz del árbol.
|
| 36 |
+
"""
|
| 37 |
+
aqui = Path(__file__).resolve()
|
| 38 |
+
for cand in (aqui.parent, *aqui.parents):
|
| 39 |
+
if (cand / "src").is_dir() and (cand / "demo").is_dir():
|
| 40 |
+
return cand
|
| 41 |
+
return aqui.parent
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
sys.path.insert(0, str(_raiz()))
|
| 45 |
+
|
| 46 |
+
from demo.portador import carga, par # noqa: E402
|
| 47 |
+
from src.gauges import (aplica_gauge, gauge_balanceado, # noqa: E402
|
| 48 |
+
gl_condicionado, gl_general, ortogonal,
|
| 49 |
+
producto_normas)
|
| 50 |
+
from src.metrics import circuito, error_relativo # noqa: E402
|
| 51 |
+
from src.quantizer import rtn_cuantiza_descuantiza # noqa: E402
|
| 52 |
+
|
| 53 |
+
DOI = "https://doi.org/10.5281/zenodo.21630534"
|
| 54 |
+
REPO = "https://github.com/mmunozpl/gauge-orbit-quantization"
|
| 55 |
+
# gr.Markdown no compone matemáticas salvo que se le declaren los
|
| 56 |
+
# delimitadores; sin esto la prosa enseña los dólares en crudo.
|
| 57 |
+
LATEX = [{"left": "$$", "right": "$$", "display": True},
|
| 58 |
+
{"left": "$", "right": "$", "display": False}]
|
| 59 |
+
# exponente publicado del ajuste conjunto sobre las dos familias
|
| 60 |
+
# (artifacts/tables/colapso_producto.csv, variable=producto, bits=4).
|
| 61 |
+
EXPONENTE = 0.9979
|
| 62 |
+
IDIOMAS = {"English": "en", "Español": "es"}
|
| 63 |
+
MODELOS = {"vitb": {"n_capas": 12, "n_cabezas": 12, "dim_cabeza": 64},
|
| 64 |
+
"pythia": {"n_capas": 24, "n_cabezas": 16, "dim_cabeza": 64}}
|
| 65 |
+
# rejilla de fuerzas que la figura recorre en vivo, por familia
|
| 66 |
+
REJILLA = {"gl_escala": [32.0, 16.0, 8.0, 4.0, 2.0],
|
| 67 |
+
"gl_kappa": [2.0, 10.0, 50.0, 200.0, 1000.0],
|
| 68 |
+
"conforme": [0.1, 0.5, 1.0, 5.0, 50.0],
|
| 69 |
+
"orto": [0.0, 1.0, 2.0, 3.0, 4.0]}
|
| 70 |
+
_CACHE: dict[str, dict] = {}
|
| 71 |
+
|
| 72 |
+
FAM = {
|
| 73 |
+
"en": {"identidad": "identity (the trained gauge)",
|
| 74 |
+
"conforme": "conformal orthogonal c·Q",
|
| 75 |
+
"orto": "Haar orthogonal",
|
| 76 |
+
"balanceado": "balanced point (constructed)",
|
| 77 |
+
"gl_escala": "general GL, by scale",
|
| 78 |
+
"gl_kappa": "general GL, by condition number κ"},
|
| 79 |
+
"es": {"identidad": "identidad (el gauge entrenado)",
|
| 80 |
+
"conforme": "conforme ortogonal c·Q",
|
| 81 |
+
"orto": "ortogonal de Haar",
|
| 82 |
+
"balanceado": "punto balanceado (construido)",
|
| 83 |
+
"gl_escala": "GL general, por escala",
|
| 84 |
+
"gl_kappa": "GL general, por número de condición κ"},
|
| 85 |
+
}
|
| 86 |
+
PARAM = {
|
| 87 |
+
"en": {"identidad": "unused", "conforme": "coefficient c",
|
| 88 |
+
"orto": "unused", "balanceado": "unused",
|
| 89 |
+
"gl_escala": "scale (low = strong gauge)",
|
| 90 |
+
"gl_kappa": "target κ"},
|
| 91 |
+
"es": {"identidad": "sin uso", "conforme": "coeficiente c",
|
| 92 |
+
"orto": "sin uso", "balanceado": "sin uso",
|
| 93 |
+
"gl_escala": "escala (baja = gauge fuerte)",
|
| 94 |
+
"gl_kappa": "κ objetivo"},
|
| 95 |
+
}
|
| 96 |
+
DEFECTO = {"identidad": 1.0, "conforme": 1.0, "orto": 1.0,
|
| 97 |
+
"balanceado": 1.0, "gl_escala": 8.0, "gl_kappa": 50.0}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def portador(modelo: str) -> dict:
|
| 101 |
+
"""carga el portador de un modelo, con caché de proceso.
|
| 102 |
+
|
| 103 |
+
args:
|
| 104 |
+
modelo: 'vitb' o 'pythia'.
|
| 105 |
+
|
| 106 |
+
returns:
|
| 107 |
+
el dict de `demo.portador.carga`.
|
| 108 |
+
"""
|
| 109 |
+
if modelo not in _CACHE:
|
| 110 |
+
_CACHE[modelo] = carga(modelo)
|
| 111 |
+
return _CACHE[modelo]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def construye_r(
|
| 115 |
+
familia: str,
|
| 116 |
+
parametro: float,
|
| 117 |
+
dim_cabeza: int,
|
| 118 |
+
gen: torch.Generator,
|
| 119 |
+
) -> torch.Tensor | None:
|
| 120 |
+
"""el gauge de una familia, en fp64.
|
| 121 |
+
|
| 122 |
+
args:
|
| 123 |
+
familia: clave de `FAM`.
|
| 124 |
+
parametro: el mando de la familia; sin uso en varias.
|
| 125 |
+
dim_cabeza: d_h.
|
| 126 |
+
gen: generador de torch.
|
| 127 |
+
|
| 128 |
+
returns:
|
| 129 |
+
tensor [d_h, d_h] en float64, o None para la identidad.
|
| 130 |
+
|
| 131 |
+
raises:
|
| 132 |
+
ValueError: si la familia no existe.
|
| 133 |
+
"""
|
| 134 |
+
if familia == "identidad":
|
| 135 |
+
return None
|
| 136 |
+
if familia == "conforme":
|
| 137 |
+
return float(parametro) * ortogonal(dim_cabeza, gen)
|
| 138 |
+
if familia == "orto":
|
| 139 |
+
return ortogonal(dim_cabeza, gen)
|
| 140 |
+
if familia == "gl_escala":
|
| 141 |
+
return gl_general(dim_cabeza, float(parametro), gen)
|
| 142 |
+
if familia == "gl_kappa":
|
| 143 |
+
return gl_condicionado(dim_cabeza, float(parametro), gen)
|
| 144 |
+
raise ValueError(f"familia desconocida: {familia}")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def mide_capa(
|
| 148 |
+
modelo: str,
|
| 149 |
+
capa: int,
|
| 150 |
+
familia: str,
|
| 151 |
+
parametro: float,
|
| 152 |
+
semilla: int,
|
| 153 |
+
bits: int,
|
| 154 |
+
) -> list[dict]:
|
| 155 |
+
"""mide todas las cabezas de una capa bajo un punto de la órbita.
|
| 156 |
+
|
| 157 |
+
por cabeza devuelve p, d(R), cond(R), el error del circuito sin
|
| 158 |
+
cuantizar ---el suelo de fp64, que la invariancia exige--- y el
|
| 159 |
+
error tras cuantizar, con su cociente contra la identidad.
|
| 160 |
+
|
| 161 |
+
args:
|
| 162 |
+
modelo: 'vitb' o 'pythia'.
|
| 163 |
+
capa: índice de capa.
|
| 164 |
+
familia: clave de `FAM`.
|
| 165 |
+
parametro: el mando de la familia.
|
| 166 |
+
semilla: semilla del muestreo.
|
| 167 |
+
bits: 4 u 8.
|
| 168 |
+
|
| 169 |
+
returns:
|
| 170 |
+
lista de dicts, una por cabeza.
|
| 171 |
+
"""
|
| 172 |
+
info = MODELOS[modelo]
|
| 173 |
+
port = portador(modelo)
|
| 174 |
+
gen = torch.Generator().manual_seed(int(semilla))
|
| 175 |
+
filas = []
|
| 176 |
+
for h in range(info["n_cabezas"]):
|
| 177 |
+
w_v, w_o = par(port, capa, h)
|
| 178 |
+
m_ref = circuito(w_v, w_o)
|
| 179 |
+
e_ident = error_relativo(rtn_cuantiza_descuantiza(w_v, bits),
|
| 180 |
+
rtn_cuantiza_descuantiza(w_o, bits),
|
| 181 |
+
m_ref)
|
| 182 |
+
if familia == "balanceado":
|
| 183 |
+
r, _ = gauge_balanceado(w_v, w_o)
|
| 184 |
+
else:
|
| 185 |
+
r = construye_r(familia, parametro, info["dim_cabeza"], gen)
|
| 186 |
+
if r is None:
|
| 187 |
+
a, b = w_v, w_o
|
| 188 |
+
else:
|
| 189 |
+
a, b = aplica_gauge(w_v, w_o, r)
|
| 190 |
+
e_exacto = error_relativo(a, b, m_ref)
|
| 191 |
+
e_quant = error_relativo(rtn_cuantiza_descuantiza(a, bits),
|
| 192 |
+
rtn_cuantiza_descuantiza(b, bits),
|
| 193 |
+
m_ref)
|
| 194 |
+
if r is None:
|
| 195 |
+
p, d_r, cond = 1.0, 0.0, 1.0
|
| 196 |
+
else:
|
| 197 |
+
p = producto_normas(w_v, w_o, r)
|
| 198 |
+
s = torch.linalg.svdvals(r)
|
| 199 |
+
d_r = float((s - s.mean()).norm() / s.norm())
|
| 200 |
+
cond = float(s[0] / s[-1])
|
| 201 |
+
filas.append({"cabeza": h, "p": p, "d_r": d_r, "cond": cond,
|
| 202 |
+
"e_exacto": e_exacto, "e_quant": e_quant,
|
| 203 |
+
"e_ident": e_ident,
|
| 204 |
+
"cociente": e_quant / e_ident})
|
| 205 |
+
return filas
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _mediana(filas: list[dict], clave: str) -> float:
|
| 209 |
+
"""mediana de una columna de `mide_capa`."""
|
| 210 |
+
v = sorted(f[clave] for f in filas)
|
| 211 |
+
n = len(v)
|
| 212 |
+
return v[n // 2] if n % 2 else 0.5 * (v[n // 2 - 1] + v[n // 2])
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def figura(
|
| 216 |
+
modelo: str,
|
| 217 |
+
capa: int,
|
| 218 |
+
familia: str,
|
| 219 |
+
semilla: int,
|
| 220 |
+
bits: int,
|
| 221 |
+
punto: tuple[float, float] | None,
|
| 222 |
+
idi: str,
|
| 223 |
+
) -> Figure:
|
| 224 |
+
"""la ley del producto recorrida en vivo, con el punto elegido.
|
| 225 |
+
|
| 226 |
+
se barre la rejilla de fuerzas de la familia sobre las cabezas de
|
| 227 |
+
la capa y se dibuja $e/e_0$ contra $p$ en log-log, junto a la recta
|
| 228 |
+
de exponente uno que el ajuste publicado da sobre 743 424 medidas.
|
| 229 |
+
|
| 230 |
+
args:
|
| 231 |
+
modelo: 'vitb' o 'pythia'.
|
| 232 |
+
capa: índice de capa.
|
| 233 |
+
familia: clave de `FAM`.
|
| 234 |
+
semilla: semilla del muestreo.
|
| 235 |
+
bits: 4 u 8.
|
| 236 |
+
punto: (p, cociente) del punto elegido, o None.
|
| 237 |
+
idi: 'en' o 'es'.
|
| 238 |
+
|
| 239 |
+
returns:
|
| 240 |
+
la figura de matplotlib.
|
| 241 |
+
"""
|
| 242 |
+
fig = Figure(figsize=(6.4, 4.2))
|
| 243 |
+
ax = fig.subplots()
|
| 244 |
+
rejilla = REJILLA.get(familia, [DEFECTO[familia]])
|
| 245 |
+
xs, ys = [], []
|
| 246 |
+
for valor in rejilla:
|
| 247 |
+
for f in mide_capa(modelo, capa, familia, valor, semilla, bits):
|
| 248 |
+
if f["p"] > 0 and f["cociente"] > 0:
|
| 249 |
+
xs.append(f["p"])
|
| 250 |
+
ys.append(f["cociente"])
|
| 251 |
+
if xs:
|
| 252 |
+
ax.scatter(xs, ys, s=14, alpha=0.45,
|
| 253 |
+
label="barrido en vivo" if idi == "es"
|
| 254 |
+
else "live sweep")
|
| 255 |
+
lo, hi = min(xs + [0.9]), max(xs + [1.1])
|
| 256 |
+
rec = [lo, hi]
|
| 257 |
+
ax.plot(rec, [x ** EXPONENTE for x in rec], color="crimson",
|
| 258 |
+
linewidth=1.4,
|
| 259 |
+
label=(f"ajuste publicado, exponente {EXPONENTE}"
|
| 260 |
+
if idi == "es"
|
| 261 |
+
else f"published fit, exponent {EXPONENTE}"))
|
| 262 |
+
if punto is not None and punto[0] > 0 and punto[1] > 0:
|
| 263 |
+
ax.scatter([punto[0]], [punto[1]], s=140, marker="*",
|
| 264 |
+
color="black", zorder=5,
|
| 265 |
+
label="tu punto" if idi == "es" else "your point")
|
| 266 |
+
ax.set_xscale("log")
|
| 267 |
+
ax.set_yscale("log")
|
| 268 |
+
ax.set_xlabel("$p(R)$")
|
| 269 |
+
ax.set_ylabel("$e/e_0$")
|
| 270 |
+
ax.set_title("La ley del producto" if idi == "es"
|
| 271 |
+
else "The product law")
|
| 272 |
+
ax.legend(fontsize=8)
|
| 273 |
+
fig.tight_layout()
|
| 274 |
+
return fig
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
LEMA = {
|
| 278 |
+
"en": ("### Same function before quantisation; different error "
|
| 279 |
+
"after it\n\n"
|
| 280 |
+
"$p(R)$ tracks the quantisation sensitivity of the "
|
| 281 |
+
"value-output factorisation."),
|
| 282 |
+
"es": ("### La misma función antes de cuantizar; distinto error "
|
| 283 |
+
"después\n\n"
|
| 284 |
+
"$p(R)$ mide la sensibilidad a la cuantización de la "
|
| 285 |
+
"factorización valor-salida."),
|
| 286 |
+
}
|
| 287 |
+
# de dónde sale cada número: la demo mezcla medida en vivo sobre una
|
| 288 |
+
# capa con el ajuste publicado sobre el barrido entero, y quien mire
|
| 289 |
+
# tiene derecho a saber cuál es cuál sin abrir el código.
|
| 290 |
+
ALCANCE = {
|
| 291 |
+
"en": ("**Live**, computed now over the chosen model, layer and "
|
| 292 |
+
"point: the table above, the per-head table and the scattered "
|
| 293 |
+
"points below. **Published**, measured offline over the full "
|
| 294 |
+
"sweep of $743\\,424$ points across two models, two bit "
|
| 295 |
+
"widths and two gauge families: the red line of exponent "
|
| 296 |
+
f"{EXPONENTE}. A live sample of one layer is not the "
|
| 297 |
+
"experiment; it is one slice of it."),
|
| 298 |
+
"es": ("**En vivo**, calculado ahora sobre el modelo, la capa y el "
|
| 299 |
+
"punto elegidos: la tabla de arriba, la tabla por cabeza y "
|
| 300 |
+
"los puntos dispersos de abajo. **Publicado**, medido aparte "
|
| 301 |
+
"sobre el barrido entero de $743\\,424$ puntos en dos "
|
| 302 |
+
"modelos, dos anchuras y dos familias de gauge: la recta roja "
|
| 303 |
+
f"de exponente {EXPONENTE}. Una muestra en vivo de una capa "
|
| 304 |
+
"no es el experimento, es una rebanada suya."),
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
T = {
|
| 308 |
+
"en": {
|
| 309 |
+
"titulo": (
|
| 310 |
+
"## Gauge equivalence does not survive quantisation\n\n"
|
| 311 |
+
"The value-output pair of an attention head is not unique: for "
|
| 312 |
+
"every invertible $R$, the substitution $W_v\\to R^\\top W_v$, "
|
| 313 |
+
"$W_O\\to W_O R^{-\\top}$ leaves the OV circuit "
|
| 314 |
+
"$M=W_O W_v$ **exactly** fixed. Pick a point of that orbit below "
|
| 315 |
+
"and watch what quantisation does to it.\n\n"
|
| 316 |
+
f"Paper and code: [repository]({REPO}) · previous work: "
|
| 317 |
+
f"[{DOI.split('/')[-1]}]({DOI})"),
|
| 318 |
+
"tab1": "The orbit", "tab2": "The switch", "tab3": "What is measured",
|
| 319 |
+
"modelo": "Model", "capa": "Layer", "familia": "Gauge family",
|
| 320 |
+
"param": "Parameter", "sem": "Seed", "bits": "Bit width",
|
| 321 |
+
"btn": "Measure this point", "fig": "The product law",
|
| 322 |
+
"alcance": "What is live and what is published",
|
| 323 |
+
"porcabeza": "Per head", "res": "Result",
|
| 324 |
+
"cab": ["head", "p(R)", "d(R)", "cond(R)", "e unquantised",
|
| 325 |
+
f"e quantised", "e / e0"],
|
| 326 |
+
},
|
| 327 |
+
"es": {
|
| 328 |
+
"titulo": (
|
| 329 |
+
"## La equivalencia de gauge no sobrevive a la cuantización\n\n"
|
| 330 |
+
"El par valor-salida de una cabeza de atención no es único: para "
|
| 331 |
+
"toda $R$ invertible, la sustitución $W_v\\to R^\\top W_v$, "
|
| 332 |
+
"$W_O\\to W_O R^{-\\top}$ deja el circuito OV $M=W_O W_v$ "
|
| 333 |
+
"**exactamente** fijo. Elige abajo un punto de esa órbita y mira "
|
| 334 |
+
"qué le hace la cuantización.\n\n"
|
| 335 |
+
f"Artículo y código: [repositorio]({REPO}) · trabajo previo: "
|
| 336 |
+
f"[{DOI.split('/')[-1]}]({DOI})"),
|
| 337 |
+
"tab1": "La órbita", "tab2": "El interruptor", "tab3": "Qué se mide",
|
| 338 |
+
"modelo": "Modelo", "capa": "Capa", "familia": "Familia de gauge",
|
| 339 |
+
"param": "Parámetro", "sem": "Semilla", "bits": "Anchura",
|
| 340 |
+
"btn": "Medir este punto", "fig": "La ley del producto",
|
| 341 |
+
"alcance": "Qué es en vivo y qué es publicado",
|
| 342 |
+
"porcabeza": "Por cabeza", "res": "Resultado",
|
| 343 |
+
"cab": ["cabeza", "p(R)", "d(R)", "cond(R)", "e sin cuantizar",
|
| 344 |
+
"e cuantizado", "e / e0"],
|
| 345 |
+
},
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
QSM = {
|
| 349 |
+
"en": (
|
| 350 |
+
"### What is measured\n\n"
|
| 351 |
+
"**The circuit error.** $e=\\lVert\\hat M-M\\rVert_F/"
|
| 352 |
+
"\\lVert M\\rVert_F$, with $\\hat M$ recomposed in fp64 from the "
|
| 353 |
+
"quantised pair. All gauge arithmetic is fp64.\n\n"
|
| 354 |
+
"**The bound.** With a per-row quantiser bounded by $\\delta$, "
|
| 355 |
+
"$e(R)\\le(\\delta_v+\\delta_O+\\delta_v\\delta_O)\\,g(R)$, and "
|
| 356 |
+
"with $g$ the only gauge-dependent factor. Normalising it by its "
|
| 357 |
+
"value at the identity gives $p=\\lVert W_OR^{-\\top}\\rVert_F"
|
| 358 |
+
"\\lVert R^{\\top}W_v\\rVert_F/"
|
| 359 |
+
"(\\lVert W_O\\rVert_F\\lVert W_v\\rVert_F)$. Two bounds do not "
|
| 360 |
+
"divide: $e/e_0\\simeq p$ holds if the effective quantisation "
|
| 361 |
+
"factor varies little across gauges. It is the hypothesis, not a "
|
| 362 |
+
"corollary.\n\n"
|
| 363 |
+
"**Two analytic ends.** The conformal orthogonal class $\\{cQ\\}$ "
|
| 364 |
+
"gives $p\\equiv1$ for every $c$ and $Q$, so it is neutral for "
|
| 365 |
+
"the bound. The "
|
| 366 |
+
"balanced point minimises $p$ over the whole orbit, and the "
|
| 367 |
+
"minimisers are that family up to a reciprocal rescaling.\n\n"
|
| 368 |
+
"**$d(R)$** is the standard deviation of the singular values over "
|
| 369 |
+
"their norm, the distance to $\\{cQ\\}$. It is exactly zero on the "
|
| 370 |
+
"neutral class.\n\n"
|
| 371 |
+
"**Nothing is reimplemented here.** The demo runs `src/gauges.py`, "
|
| 372 |
+
"`src/quantizer.py` and `src/metrics.py` on a light carrier of the "
|
| 373 |
+
"value-output sector, and `scripts/paridad_demo.py` certifies it "
|
| 374 |
+
"cell by cell against the published sweeps.\n\n"
|
| 375 |
+
"**What this demo does not do.** It does not run forwards. The "
|
| 376 |
+
"end-to-end numbers in the second tab were measured offline over "
|
| 377 |
+
"the 5000 images of the clean ImageNet-100 validation split."),
|
| 378 |
+
"es": (
|
| 379 |
+
"### Qué se mide\n\n"
|
| 380 |
+
"**El error del circuito.** $e=\\lVert\\hat M-M\\rVert_F/"
|
| 381 |
+
"\\lVert M\\rVert_F$, con $\\hat M$ recompuesto en fp64 desde el "
|
| 382 |
+
"par cuantizado. Toda la aritmética de gauges es fp64.\n\n"
|
| 383 |
+
"**La cota.** Con un cuantizador por fila acotado por $\\delta$, "
|
| 384 |
+
"$e(R)\\le(\\delta_v+\\delta_O+\\delta_v\\delta_O)\\,g(R)$, y "
|
| 385 |
+
"con $g$ el único factor que depende del gauge. Normalizarlo por "
|
| 386 |
+
"su valor en la identidad da $p=\\lVert W_OR^{-\\top}\\rVert_F"
|
| 387 |
+
"\\lVert R^{\\top}W_v\\rVert_F/"
|
| 388 |
+
"(\\lVert W_O\\rVert_F\\lVert W_v\\rVert_F)$. Dos cotas no se "
|
| 389 |
+
"dividen: $e/e_0\\simeq p$ vale si el factor efectivo de "
|
| 390 |
+
"cuantización varía poco entre gauges. Es la hipótesis, no un "
|
| 391 |
+
"corolario.\n\n"
|
| 392 |
+
"**Dos extremos analíticos.** La clase conforme ortogonal "
|
| 393 |
+
"$\\{cQ\\}$ da $p\\equiv1$ para toda $c$ y toda $Q$, así que es "
|
| 394 |
+
"neutra para la cota. El punto balanceado minimiza $p$ sobre la "
|
| 395 |
+
"órbita entera, "
|
| 396 |
+
"y los mínimos son esa familia salvo un escalado recíproco.\n\n"
|
| 397 |
+
"**$d(R)$** es la desviación típica de los valores singulares "
|
| 398 |
+
"partida por su norma, la distancia a $\\{cQ\\}$. Vale cero "
|
| 399 |
+
"exactamente sobre la clase neutra.\n\n"
|
| 400 |
+
"**Aquí no se reimplementa nada.** La demo ejecuta "
|
| 401 |
+
"`src/gauges.py`, `src/quantizer.py` y `src/metrics.py` sobre un "
|
| 402 |
+
"portador ligero del sector valor-salida, y "
|
| 403 |
+
"`scripts/paridad_demo.py` la certifica celda a celda contra los "
|
| 404 |
+
"barridos publicados.\n\n"
|
| 405 |
+
"**Qué no hace esta demo.** No corre forwards. Las cifras "
|
| 406 |
+
"end-to-end de la segunda pestaña se midieron aparte sobre las "
|
| 407 |
+
"5000 imágenes del val limpio de ImageNet-100."),
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def tabla_e2e(idi: str) -> str:
|
| 412 |
+
"""la validación end-to-end medida, como markdown.
|
| 413 |
+
|
| 414 |
+
args:
|
| 415 |
+
idi: 'en' o 'es'.
|
| 416 |
+
|
| 417 |
+
returns:
|
| 418 |
+
el bloque de markdown de la pestaña del interruptor.
|
| 419 |
+
"""
|
| 420 |
+
import csv
|
| 421 |
+
|
| 422 |
+
ruta = _raiz() / "artifacts" / "tables" / "validacion_e2e.csv"
|
| 423 |
+
if not ruta.exists():
|
| 424 |
+
return "_sin tabla end-to-end_" if idi == "es" else "_no table_"
|
| 425 |
+
filas = list(csv.DictReader(ruta.open()))
|
| 426 |
+
nom = {"identidad_fp32": ("Identity, not quantised",
|
| 427 |
+
"Identidad, sin cuantizar"),
|
| 428 |
+
"identidad": ("Identity, int4", "Identidad, int4"),
|
| 429 |
+
"mejor_orto_muestreado": ("Best sampled orthogonal, int4",
|
| 430 |
+
"Mejor ortogonal muestreado, int4"),
|
| 431 |
+
"peor_orto_muestreado": ("Worst sampled orthogonal, int4",
|
| 432 |
+
"Peor ortogonal muestreado, int4"),
|
| 433 |
+
"balanceado": ("Balanced point, int4",
|
| 434 |
+
"Punto balanceado, int4"),
|
| 435 |
+
"gl_escala2_fp32": ("GL scale 2, NOT quantised",
|
| 436 |
+
"GL escala 2, SIN cuantizar"),
|
| 437 |
+
"gl_escala2": ("GL scale 2, int4", "GL escala 2, int4")}
|
| 438 |
+
cab = ("| Condition | Top-1 |\n|---|---|"
|
| 439 |
+
if idi == "en" else "| Condición | Top-1 |\n|---|---|")
|
| 440 |
+
j = 0 if idi == "en" else 1
|
| 441 |
+
cuerpo = "\n".join(
|
| 442 |
+
f"| {nom.get(f['condicion'], (f['condicion'],) * 2)[j]} "
|
| 443 |
+
f"| {float(f['top1']):.4f} |" for f in filas)
|
| 444 |
+
intro = {
|
| 445 |
+
"en": ("### The switch\n\nSeven forwards of ViT-B/16 over the "
|
| 446 |
+
"5000 images of the clean ImageNet-100 validation split. "
|
| 447 |
+
"The same gauge is measured twice, with and without "
|
| 448 |
+
"quantisation. That pair of rows is the whole argument: "
|
| 449 |
+
"the gauge leaves the function alone and quantisation "
|
| 450 |
+
"destroys it. Unquantised, that gauge predicts the same "
|
| 451 |
+
"class as the identity on **all 5000 images**, without a "
|
| 452 |
+
"single discrepancy; quantised, it agrees on 44.\n\n"),
|
| 453 |
+
"es": ("### El interruptor\n\nSiete forwards de ViT-B/16 sobre "
|
| 454 |
+
"las 5000 imágenes del val limpio de ImageNet-100. El "
|
| 455 |
+
"mismo gauge se mide dos veces, con y sin cuantizar. Ese "
|
| 456 |
+
"par de filas es el argumento entero: el gauge deja la "
|
| 457 |
+
"función quieta y la cuantización la destruye. Sin "
|
| 458 |
+
"cuantizar, ese gauge predice la misma clase que la "
|
| 459 |
+
"identidad en **las 5000 imágenes**, sin una sola "
|
| 460 |
+
"discrepancia; cuantizado, coincide en 44.\n\n"),
|
| 461 |
+
}
|
| 462 |
+
return intro[idi] + cab + "\n" + cuerpo
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def orbita(
|
| 466 |
+
idioma: str,
|
| 467 |
+
modelo: str,
|
| 468 |
+
capa: int,
|
| 469 |
+
familia: str,
|
| 470 |
+
parametro: float,
|
| 471 |
+
semilla: int,
|
| 472 |
+
bits: int,
|
| 473 |
+
):
|
| 474 |
+
"""mide el punto elegido y compone resultado, tabla y figura.
|
| 475 |
+
|
| 476 |
+
args:
|
| 477 |
+
idioma: clave de `IDIOMAS`.
|
| 478 |
+
modelo: 'vitb' o 'pythia'.
|
| 479 |
+
capa: índice de capa.
|
| 480 |
+
familia: clave de `FAM`.
|
| 481 |
+
parametro: el mando de la familia.
|
| 482 |
+
semilla: semilla del muestreo.
|
| 483 |
+
bits: 4 u 8.
|
| 484 |
+
|
| 485 |
+
returns:
|
| 486 |
+
(markdown del resultado, filas de la tabla, figura).
|
| 487 |
+
"""
|
| 488 |
+
idi = IDIOMAS[idioma]
|
| 489 |
+
filas = mide_capa(modelo, int(capa), familia, parametro,
|
| 490 |
+
int(semilla), int(bits))
|
| 491 |
+
p_med = _mediana(filas, "p")
|
| 492 |
+
coc = _mediana(filas, "cociente")
|
| 493 |
+
e_ex = max(f["e_exacto"] for f in filas)
|
| 494 |
+
e_q = _mediana(filas, "e_quant")
|
| 495 |
+
d_med = _mediana(filas, "d_r")
|
| 496 |
+
cond_med = _mediana(filas, "cond")
|
| 497 |
+
if idi == "es":
|
| 498 |
+
res = (
|
| 499 |
+
f"**En vivo** · {FAM['es'][familia]}, capa {int(capa)}, "
|
| 500 |
+
f"int{int(bits)}. Medianas sobre las {len(filas)} cabezas "
|
| 501 |
+
f"de esa capa.\n\n"
|
| 502 |
+
f"| | |\n|---|---|\n"
|
| 503 |
+
f"| $p(R)$ | {p_med:.4f} |\n"
|
| 504 |
+
f"| $d(R)$, distancia a la clase neutra | {d_med:.4f} |\n"
|
| 505 |
+
f"| $\\operatorname{{cond}}(R)$ | {cond_med:.3f} |\n"
|
| 506 |
+
f"| error del circuito **sin cuantizar** (peor cabeza) "
|
| 507 |
+
f"| {e_ex:.2e} |\n"
|
| 508 |
+
f"| error del circuito en int{int(bits)} | {e_q:.4f} |\n"
|
| 509 |
+
f"| $e/e_0$ medido | {coc:.4f} |\n"
|
| 510 |
+
f"| $e/e_0$ que la cota predice | {p_med:.4f} |\n\n"
|
| 511 |
+
f"Sin cuantizar el error se queda en el suelo de fp64: el "
|
| 512 |
+
f"gauge no toca la función. Cuantizado, el mismo punto "
|
| 513 |
+
f"multiplica el error por {coc:.2f}.")
|
| 514 |
+
else:
|
| 515 |
+
res = (
|
| 516 |
+
f"**Live** · {FAM['en'][familia]}, layer {int(capa)}, "
|
| 517 |
+
f"int{int(bits)}. Medians over the {len(filas)} heads of "
|
| 518 |
+
f"that layer.\n\n"
|
| 519 |
+
f"| | |\n|---|---|\n"
|
| 520 |
+
f"| $p(R)$ | {p_med:.4f} |\n"
|
| 521 |
+
f"| $d(R)$, distance to the neutral class | {d_med:.4f} |\n"
|
| 522 |
+
f"| $\\operatorname{{cond}}(R)$ | {cond_med:.3f} |\n"
|
| 523 |
+
f"| circuit error **unquantised** (worst head) "
|
| 524 |
+
f"| {e_ex:.2e} |\n"
|
| 525 |
+
f"| circuit error at int{int(bits)} | {e_q:.4f} |\n"
|
| 526 |
+
f"| $e/e_0$ measured | {coc:.4f} |\n"
|
| 527 |
+
f"| $e/e_0$ predicted by the bound | {p_med:.4f} |\n\n"
|
| 528 |
+
f"Unquantised the error stays at the fp64 floor: the gauge "
|
| 529 |
+
f"does not touch the function. Quantised, the same point "
|
| 530 |
+
f"multiplies the error by {coc:.2f}.")
|
| 531 |
+
tabla = [[f["cabeza"], round(f["p"], 4), round(f["d_r"], 4),
|
| 532 |
+
round(f["cond"], 3), f"{f['e_exacto']:.2e}",
|
| 533 |
+
round(f["e_quant"], 5), round(f["cociente"], 4)]
|
| 534 |
+
for f in filas]
|
| 535 |
+
fig = figura(modelo, int(capa), familia, int(semilla), int(bits),
|
| 536 |
+
(p_med, coc), idi)
|
| 537 |
+
return res, tabla, fig
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def capas_de(modelo: str):
|
| 541 |
+
"""actualiza el desplegable de capas al cambiar de modelo.
|
| 542 |
+
|
| 543 |
+
args:
|
| 544 |
+
modelo: 'vitb' o 'pythia'.
|
| 545 |
+
|
| 546 |
+
returns:
|
| 547 |
+
la actualización de gradio para el desplegable.
|
| 548 |
+
"""
|
| 549 |
+
n = MODELOS[modelo]["n_capas"]
|
| 550 |
+
return gr.update(choices=list(range(n)), value=min(5, n - 1))
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
def cambia_familia(idioma: str, familia: str):
|
| 554 |
+
"""reetiqueta el mando de la familia y le pone su valor por defecto.
|
| 555 |
+
|
| 556 |
+
args:
|
| 557 |
+
idioma: clave de `IDIOMAS`.
|
| 558 |
+
familia: clave de `FAM`.
|
| 559 |
+
|
| 560 |
+
returns:
|
| 561 |
+
la actualización de gradio para el mando.
|
| 562 |
+
"""
|
| 563 |
+
idi = IDIOMAS[idioma]
|
| 564 |
+
return gr.update(label=PARAM[idi][familia], value=DEFECTO[familia])
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def cambia_idioma(idioma: str, familia: str):
|
| 568 |
+
"""reetiqueta la interfaz entera.
|
| 569 |
+
|
| 570 |
+
args:
|
| 571 |
+
idioma: clave de `IDIOMAS`.
|
| 572 |
+
familia: clave de `FAM` vigente.
|
| 573 |
+
|
| 574 |
+
returns:
|
| 575 |
+
la tupla de actualizaciones, en el orden del cableado.
|
| 576 |
+
"""
|
| 577 |
+
idi = IDIOMAS[idioma]
|
| 578 |
+
d = T[idi]
|
| 579 |
+
fam_op = [(v, k) for k, v in FAM[idi].items()]
|
| 580 |
+
return (gr.update(value=d["titulo"]),
|
| 581 |
+
gr.update(value=LEMA[idi]),
|
| 582 |
+
gr.update(label=d["modelo"]),
|
| 583 |
+
gr.update(label=d["capa"]),
|
| 584 |
+
gr.update(choices=fam_op, label=d["familia"]),
|
| 585 |
+
gr.update(label=PARAM[idi][familia]),
|
| 586 |
+
gr.update(label=d["sem"]),
|
| 587 |
+
gr.update(label=d["bits"]),
|
| 588 |
+
gr.update(value=d["btn"]),
|
| 589 |
+
gr.update(label=d["res"]),
|
| 590 |
+
gr.update(headers=d["cab"], label=d["porcabeza"]),
|
| 591 |
+
gr.update(label=d["fig"]),
|
| 592 |
+
gr.update(value=ALCANCE[idi], label=d["alcance"]),
|
| 593 |
+
gr.update(value=tabla_e2e(idi)),
|
| 594 |
+
gr.update(value=QSM[idi]),
|
| 595 |
+
gr.update(label=d["tab1"]),
|
| 596 |
+
gr.update(label=d["tab2"]),
|
| 597 |
+
gr.update(label=d["tab3"]))
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
TITULO = ("Gauge equivalence does not survive quantisation - "
|
| 601 |
+
"La equivalencia de gauge no sobrevive a la cuantización")
|
| 602 |
+
|
| 603 |
+
with gr.Blocks(title=TITULO) as demo:
|
| 604 |
+
idi_o = gr.Radio(list(IDIOMAS), value="English",
|
| 605 |
+
label="Idioma / Language")
|
| 606 |
+
cab = gr.Markdown(T["en"]["titulo"], latex_delimiters=LATEX)
|
| 607 |
+
lema = gr.Markdown(LEMA["en"], latex_delimiters=LATEX)
|
| 608 |
+
with gr.Tab(T["en"]["tab1"]) as pes1:
|
| 609 |
+
with gr.Row():
|
| 610 |
+
mod_o = gr.Dropdown(list(MODELOS), value="vitb",
|
| 611 |
+
label=T["en"]["modelo"])
|
| 612 |
+
capa_o = gr.Dropdown(list(range(12)), value=5,
|
| 613 |
+
label=T["en"]["capa"])
|
| 614 |
+
fam_o = gr.Dropdown([(v, k) for k, v in FAM["en"].items()],
|
| 615 |
+
value="gl_escala",
|
| 616 |
+
label=T["en"]["familia"])
|
| 617 |
+
par_o = gr.Number(value=8.0, label=PARAM["en"]["gl_escala"])
|
| 618 |
+
sem_o = gr.Number(value=0, precision=0, label=T["en"]["sem"])
|
| 619 |
+
bit_o = gr.Radio([4, 8], value=4, label=T["en"]["bits"])
|
| 620 |
+
btn_o = gr.Button(T["en"]["btn"], variant="primary")
|
| 621 |
+
res_o = gr.Markdown(label=T["en"]["res"], latex_delimiters=LATEX)
|
| 622 |
+
fig_o = gr.Plot(label=T["en"]["fig"])
|
| 623 |
+
alc_o = gr.Markdown(ALCANCE["en"], label=T["en"]["alcance"],
|
| 624 |
+
latex_delimiters=LATEX)
|
| 625 |
+
tab_o = gr.Dataframe(headers=T["en"]["cab"],
|
| 626 |
+
label=T["en"]["porcabeza"])
|
| 627 |
+
mod_o.change(capas_de, mod_o, capa_o)
|
| 628 |
+
fam_o.change(cambia_familia, [idi_o, fam_o], par_o)
|
| 629 |
+
btn_o.click(orbita,
|
| 630 |
+
[idi_o, mod_o, capa_o, fam_o, par_o, sem_o, bit_o],
|
| 631 |
+
[res_o, tab_o, fig_o])
|
| 632 |
+
with gr.Tab(T["en"]["tab2"]) as pes2:
|
| 633 |
+
e2e_o = gr.Markdown(tabla_e2e("en"), latex_delimiters=LATEX)
|
| 634 |
+
with gr.Tab(T["en"]["tab3"]) as pes3:
|
| 635 |
+
qsm_o = gr.Markdown(QSM["en"], latex_delimiters=LATEX)
|
| 636 |
+
idi_o.change(cambia_idioma, [idi_o, fam_o],
|
| 637 |
+
[cab, lema, mod_o, capa_o, fam_o, par_o, sem_o,
|
| 638 |
+
bit_o, btn_o, res_o, tab_o, fig_o, alc_o, e2e_o,
|
| 639 |
+
qsm_o, pes1, pes2, pes3])
|
| 640 |
+
|
| 641 |
+
if __name__ == "__main__":
|
| 642 |
+
demo.launch()
|
artifacts/demo/sector_vo_pythia.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"modelo": "pythia",
|
| 3 |
+
"n_capas": 24,
|
| 4 |
+
"n_cabezas": 16,
|
| 5 |
+
"dim_cabeza": 64,
|
| 6 |
+
"formas": {
|
| 7 |
+
"w_v": [
|
| 8 |
+
24,
|
| 9 |
+
16,
|
| 10 |
+
64,
|
| 11 |
+
1024
|
| 12 |
+
],
|
| 13 |
+
"w_o": [
|
| 14 |
+
24,
|
| 15 |
+
16,
|
| 16 |
+
1024,
|
| 17 |
+
64
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
"dtype": "float32 en disco, float64 al cargar",
|
| 21 |
+
"sha256": "60d47a1947aa620dbca68fae9d7a832405f4f554d6cd17a5b83705b41061966e",
|
| 22 |
+
"bytes": 201326760
|
| 23 |
+
}
|
artifacts/demo/sector_vo_pythia.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60d47a1947aa620dbca68fae9d7a832405f4f554d6cd17a5b83705b41061966e
|
| 3 |
+
size 201326760
|
artifacts/demo/sector_vo_vitb.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"modelo": "vitb",
|
| 3 |
+
"n_capas": 12,
|
| 4 |
+
"n_cabezas": 12,
|
| 5 |
+
"dim_cabeza": 64,
|
| 6 |
+
"formas": {
|
| 7 |
+
"w_v": [
|
| 8 |
+
12,
|
| 9 |
+
12,
|
| 10 |
+
64,
|
| 11 |
+
768
|
| 12 |
+
],
|
| 13 |
+
"w_o": [
|
| 14 |
+
12,
|
| 15 |
+
12,
|
| 16 |
+
768,
|
| 17 |
+
64
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
"dtype": "float32 en disco, float64 al cargar",
|
| 21 |
+
"sha256": "10ef9ef42a14cd3e6d3ddcd3ee3bd84a80dd3242ee3a2891766ead931607e4ad",
|
| 22 |
+
"bytes": 56623272
|
| 23 |
+
}
|
artifacts/demo/sector_vo_vitb.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:10ef9ef42a14cd3e6d3ddcd3ee3bd84a80dd3242ee3a2891766ead931607e4ad
|
| 3 |
+
size 56623272
|
artifacts/tables/validacion_e2e.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
condicion,bits,top1,desv_invariancia,post_hoc_declarado
|
| 2 |
+
identidad_fp32,,0.9314,,False
|
| 3 |
+
identidad,4,0.9308,,False
|
| 4 |
+
mejor_orto_muestreado,4,0.9306,3.976204823175067e-07,False
|
| 5 |
+
peor_orto_muestreado,4,0.9298,4.014851526790153e-07,False
|
| 6 |
+
balanceado,4,0.9306,4.2091673435606936e-07,True
|
| 7 |
+
gl_escala2_fp32,,0.9314,1.1825894034700468e-05,True
|
| 8 |
+
gl_escala2,4,0.0082,1.1825894034700468e-05,True
|
demo/portador.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""carga del portador del sector valor-salida.
|
| 2 |
+
|
| 3 |
+
lo construye `scripts/extraer_sector_vo.py`. aquí solo se lee y se
|
| 4 |
+
verifica, porque el Space no lleva los modelos originales.
|
| 5 |
+
|
| 6 |
+
la regla de esta demo: no se reimplementa nada. el portador devuelve
|
| 7 |
+
los mismos tensores que `src.weights.w_v_w_o_*` entregaba al barrido, y
|
| 8 |
+
sobre ellos corren `src.gauges`, `src.quantizer` y `src.metrics` sin
|
| 9 |
+
un byte cambiado. si la demo y el paper discreparan, el culpable sería
|
| 10 |
+
el portador, y por eso lleva manifiesto con sha256.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
from safetensors.torch import load_file
|
| 18 |
+
|
| 19 |
+
def _raiz() -> Path:
|
| 20 |
+
"""la raíz del árbol, igual en el repo que en el Space.
|
| 21 |
+
|
| 22 |
+
returns:
|
| 23 |
+
el directorio que contiene `src` y `demo`.
|
| 24 |
+
"""
|
| 25 |
+
aqui = Path(__file__).resolve()
|
| 26 |
+
for cand in (aqui.parent, *aqui.parents):
|
| 27 |
+
if (cand / "src").is_dir() and (cand / "demo").is_dir():
|
| 28 |
+
return cand
|
| 29 |
+
return aqui.parent
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
RAIZ_DEFECTO = _raiz() / "artifacts" / "demo"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _sha256(ruta: Path) -> str:
|
| 36 |
+
"""digest del fichero del portador.
|
| 37 |
+
|
| 38 |
+
args:
|
| 39 |
+
ruta: fichero a resumir.
|
| 40 |
+
|
| 41 |
+
returns:
|
| 42 |
+
sha256 en hexadecimal.
|
| 43 |
+
"""
|
| 44 |
+
h = hashlib.sha256()
|
| 45 |
+
with ruta.open("rb") as fh:
|
| 46 |
+
for bloque in iter(lambda: fh.read(1 << 20), b""):
|
| 47 |
+
h.update(bloque)
|
| 48 |
+
return h.hexdigest()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def carga(modelo: str, raiz: Path | None = None) -> dict:
|
| 52 |
+
"""lee el portador de un modelo y lo sube a float64.
|
| 53 |
+
|
| 54 |
+
el fichero guarda float32, que es la dtype de origen; subirlo a
|
| 55 |
+
float64 reproduce bit a bit lo que la extracción del barrido
|
| 56 |
+
entregaba, y deja la aritmética de gauges en fp64.
|
| 57 |
+
|
| 58 |
+
args:
|
| 59 |
+
modelo: 'vitb' o 'pythia'.
|
| 60 |
+
raiz: directorio del portador; por defecto artifacts/demo.
|
| 61 |
+
|
| 62 |
+
returns:
|
| 63 |
+
dict con 'w_v' [capas, cabezas, d_h, d], 'w_o' [capas,
|
| 64 |
+
cabezas, d, d_h] en float64, y el manifiesto bajo 'meta'.
|
| 65 |
+
|
| 66 |
+
raises:
|
| 67 |
+
FileNotFoundError: si falta el portador o su manifiesto.
|
| 68 |
+
"""
|
| 69 |
+
raiz = Path(raiz) if raiz is not None else RAIZ_DEFECTO
|
| 70 |
+
fichero = raiz / f"sector_vo_{modelo}.safetensors"
|
| 71 |
+
manif = raiz / f"sector_vo_{modelo}.json"
|
| 72 |
+
if not fichero.exists() or not manif.exists():
|
| 73 |
+
raise FileNotFoundError(
|
| 74 |
+
f"falta el portador de {modelo} en {raiz}; constrúyelo con "
|
| 75 |
+
f"scripts/extraer_sector_vo.py --modelo {modelo}")
|
| 76 |
+
meta = json.loads(manif.read_text())
|
| 77 |
+
t = load_file(str(fichero))
|
| 78 |
+
return {"w_v": t["w_v"].double(), "w_o": t["w_o"].double(),
|
| 79 |
+
"meta": meta}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def verifica_manifiesto(modelo: str, raiz: Path | None = None) -> None:
|
| 83 |
+
"""comprueba sha256 y formas contra el manifiesto.
|
| 84 |
+
|
| 85 |
+
args:
|
| 86 |
+
modelo: 'vitb' o 'pythia'.
|
| 87 |
+
raiz: directorio del portador.
|
| 88 |
+
|
| 89 |
+
raises:
|
| 90 |
+
RuntimeError: si el digest o alguna forma no coinciden.
|
| 91 |
+
"""
|
| 92 |
+
raiz = Path(raiz) if raiz is not None else RAIZ_DEFECTO
|
| 93 |
+
fichero = raiz / f"sector_vo_{modelo}.safetensors"
|
| 94 |
+
meta = json.loads((raiz / f"sector_vo_{modelo}.json").read_text())
|
| 95 |
+
digest = _sha256(fichero)
|
| 96 |
+
if digest != meta["sha256"]:
|
| 97 |
+
raise RuntimeError(
|
| 98 |
+
f"el portador de {modelo} no es el del manifiesto: "
|
| 99 |
+
f"{digest[:16]} frente a {meta['sha256'][:16]}")
|
| 100 |
+
t = load_file(str(fichero))
|
| 101 |
+
for clave, forma in meta["formas"].items():
|
| 102 |
+
if list(t[clave].shape) != forma:
|
| 103 |
+
raise RuntimeError(
|
| 104 |
+
f"forma inesperada en {clave}: {list(t[clave].shape)} "
|
| 105 |
+
f"frente a {forma}")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def par(portador: dict, capa: int, cabeza: int) -> tuple:
|
| 109 |
+
"""(w_v, w_o) de una cabeza, en float64.
|
| 110 |
+
|
| 111 |
+
args:
|
| 112 |
+
portador: el dict que devuelve `carga`.
|
| 113 |
+
capa: índice de capa.
|
| 114 |
+
cabeza: índice de cabeza.
|
| 115 |
+
|
| 116 |
+
returns:
|
| 117 |
+
(w_v [d_h, d], w_o [d, d_h]) en float64.
|
| 118 |
+
"""
|
| 119 |
+
return portador["w_v"][capa, cabeza], portador["w_o"][capa, cabeza]
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# entorno del Space: la demo es álgebra sobre pesos. no necesita
|
| 2 |
+
# transformers, timm ni pandas; torch en su rueda de cpu basta.
|
| 3 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 4 |
+
gradio==6.22.0
|
| 5 |
+
torch==2.11.0
|
| 6 |
+
safetensors==0.6.2
|
| 7 |
+
numpy
|
| 8 |
+
matplotlib
|
src/__init__.py
ADDED
|
File without changes
|
src/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""cargador único de configuraciones YAML."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import yaml
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class _Config(dict):
|
| 10 |
+
"""configuración que exige valor a las rutas sin declarar."""
|
| 11 |
+
|
| 12 |
+
def __getitem__(self, clave: str) -> object:
|
| 13 |
+
"""devuelve el valor de una clave, o explica que falta.
|
| 14 |
+
|
| 15 |
+
args:
|
| 16 |
+
clave: nombre de la entrada de configuración.
|
| 17 |
+
|
| 18 |
+
returns:
|
| 19 |
+
el valor asociado a la clave.
|
| 20 |
+
|
| 21 |
+
raises:
|
| 22 |
+
ValueError: si la clave existe pero está sin declarar.
|
| 23 |
+
"""
|
| 24 |
+
valor = super().__getitem__(clave)
|
| 25 |
+
if valor in (None, ""):
|
| 26 |
+
raise ValueError(
|
| 27 |
+
f"«{clave}» está sin declarar: es una ruta del árbol "
|
| 28 |
+
f"propio y no se versiona. Se declara en el fichero "
|
| 29 |
+
f"«.local.yaml» que acompaña a la configuración, o por "
|
| 30 |
+
f"variable de entorno. Ver LEEME.md."
|
| 31 |
+
)
|
| 32 |
+
return valor
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _lee(ruta: Path) -> dict:
|
| 36 |
+
"""lee un yaml y devuelve su contenido, vacío si no trae nada.
|
| 37 |
+
|
| 38 |
+
args:
|
| 39 |
+
ruta: ruta al fichero .yaml.
|
| 40 |
+
|
| 41 |
+
returns:
|
| 42 |
+
diccionario con el contenido del yaml.
|
| 43 |
+
"""
|
| 44 |
+
with open(ruta, encoding="utf-8") as fh:
|
| 45 |
+
return yaml.safe_load(fh) or {}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _expande(valor: str | None) -> str | None:
|
| 49 |
+
"""expande variables de entorno en un valor de texto.
|
| 50 |
+
|
| 51 |
+
args:
|
| 52 |
+
valor: entrada de la configuración.
|
| 53 |
+
|
| 54 |
+
returns:
|
| 55 |
+
el valor con las variables de entorno ya sustituidas.
|
| 56 |
+
"""
|
| 57 |
+
return os.path.expandvars(valor) if isinstance(valor, str) else valor
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_config(path: str) -> dict:
|
| 61 |
+
"""lee un yaml y le superpone su variante local, si existe.
|
| 62 |
+
|
| 63 |
+
las rutas que apuntan al árbol propio no se versionan: las aporta
|
| 64 |
+
el fichero «<nombre>.local.yaml» que acompaña al de la
|
| 65 |
+
configuración, con prioridad sobre él.
|
| 66 |
+
|
| 67 |
+
args:
|
| 68 |
+
path: ruta al fichero .yaml.
|
| 69 |
+
|
| 70 |
+
returns:
|
| 71 |
+
configuración ya superpuesta y con las variables de entorno
|
| 72 |
+
expandidas.
|
| 73 |
+
"""
|
| 74 |
+
base = Path(path)
|
| 75 |
+
cfg = _lee(base)
|
| 76 |
+
local = base.with_suffix(".local.yaml")
|
| 77 |
+
if local.exists():
|
| 78 |
+
cfg.update(_lee(local))
|
| 79 |
+
return _Config({k: _expande(v) for k, v in cfg.items()})
|
src/datos.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""loader mínimo del val limpio de imagenet-100, solo para la
|
| 2 |
+
validación end-to-end (sec. 2.4 del spec). reimplementación
|
| 3 |
+
autocontenida (val-only) del loader ya usado en el paper 1.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch.utils.data import DataLoader, Dataset
|
| 10 |
+
from torchvision import datasets, transforms
|
| 11 |
+
|
| 12 |
+
_NORM = transforms.Normalize(
|
| 13 |
+
mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class _RemapEtiquetas(Dataset):
|
| 17 |
+
"""envuelve un imagefolder filtrado a los wnids canónicos y
|
| 18 |
+
remapea las etiquetas al orden de la lista (0..99)."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, base: datasets.ImageFolder,
|
| 21 |
+
indices: list[int], mapa: dict[int, int]) -> None:
|
| 22 |
+
self.base = base
|
| 23 |
+
self.indices = indices
|
| 24 |
+
self.mapa = mapa
|
| 25 |
+
|
| 26 |
+
def __len__(self) -> int:
|
| 27 |
+
return len(self.indices)
|
| 28 |
+
|
| 29 |
+
def __getitem__(self, i: int) -> tuple[torch.Tensor, int]:
|
| 30 |
+
img, lbl = self.base[self.indices[i]]
|
| 31 |
+
return img, self.mapa[int(lbl)]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def val_loader_imagenet100(
|
| 35 |
+
root: str,
|
| 36 |
+
wnids_path: str,
|
| 37 |
+
image_size: int = 224,
|
| 38 |
+
batch_size: int = 128,
|
| 39 |
+
num_workers: int = 8,
|
| 40 |
+
) -> DataLoader:
|
| 41 |
+
"""loader del split val de imagenet-100, orden canónico cmc.
|
| 42 |
+
|
| 43 |
+
args:
|
| 44 |
+
root: carpeta con `val/<wnid>/...` (estructura imagefolder).
|
| 45 |
+
wnids_path: fichero con un synset por línea, orden canónico.
|
| 46 |
+
image_size: lado del crop final.
|
| 47 |
+
batch_size: tamaño de lote.
|
| 48 |
+
num_workers: procesos de carga.
|
| 49 |
+
|
| 50 |
+
returns:
|
| 51 |
+
dataloader sin barajar sobre el val completo.
|
| 52 |
+
"""
|
| 53 |
+
wnids = [ln.strip() for ln in Path(wnids_path).read_text().splitlines()
|
| 54 |
+
if ln.strip()]
|
| 55 |
+
tfm = transforms.Compose([
|
| 56 |
+
transforms.Resize(int(image_size * 256 / 224)),
|
| 57 |
+
transforms.CenterCrop(image_size),
|
| 58 |
+
transforms.ToTensor(),
|
| 59 |
+
_NORM,
|
| 60 |
+
])
|
| 61 |
+
base = datasets.ImageFolder(Path(root) / "val", transform=tfm)
|
| 62 |
+
wnid_a_idx = {w: i for i, w in enumerate(wnids)}
|
| 63 |
+
orig_a_nuevo = {
|
| 64 |
+
orig: wnid_a_idx[synset]
|
| 65 |
+
for synset, orig in base.class_to_idx.items()
|
| 66 |
+
if synset in wnid_a_idx
|
| 67 |
+
}
|
| 68 |
+
indices = [i for i, (_, lbl) in enumerate(base.samples)
|
| 69 |
+
if lbl in orig_a_nuevo]
|
| 70 |
+
ds = _RemapEtiquetas(base, indices, orig_a_nuevo)
|
| 71 |
+
return DataLoader(ds, batch_size=batch_size, shuffle=False,
|
| 72 |
+
num_workers=num_workers, pin_memory=True)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@torch.no_grad()
|
| 76 |
+
def top1(modelo: torch.nn.Module, loader: DataLoader,
|
| 77 |
+
disp: str = "cuda") -> float:
|
| 78 |
+
"""exactitud top-1 del modelo sobre el loader.
|
| 79 |
+
|
| 80 |
+
args:
|
| 81 |
+
modelo: en eval, sobre `disp`.
|
| 82 |
+
loader: dataloader de validación.
|
| 83 |
+
disp: dispositivo.
|
| 84 |
+
|
| 85 |
+
returns:
|
| 86 |
+
fracción de aciertos.
|
| 87 |
+
"""
|
| 88 |
+
modelo.eval()
|
| 89 |
+
aciertos, total = 0, 0
|
| 90 |
+
for imgs, lbls in loader:
|
| 91 |
+
logits = modelo(imgs.to(disp, non_blocking=True))
|
| 92 |
+
pred = logits.argmax(dim=1).cpu()
|
| 93 |
+
aciertos += int((pred == lbls).sum())
|
| 94 |
+
total += int(lbls.numel())
|
| 95 |
+
return aciertos / total
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@torch.no_grad()
|
| 99 |
+
def predicciones_y_aciertos(
|
| 100 |
+
modelo: torch.nn.Module,
|
| 101 |
+
loader: DataLoader,
|
| 102 |
+
disp: str = "cuda",
|
| 103 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 104 |
+
"""clase predicha y acierto por imagen, en el orden del loader.
|
| 105 |
+
|
| 106 |
+
las dos cosas, y no solo el acierto, porque responden a preguntas
|
| 107 |
+
distintas. el acierto sostiene el contraste pareado de mcnemar:
|
| 108 |
+
dos condiciones sobre las mismas imágenes se comparan por sus
|
| 109 |
+
discordantes. la clase predicha sostiene una afirmación más
|
| 110 |
+
fuerte: cero discordantes de mcnemar dice que ambos modelos
|
| 111 |
+
aciertan y fallan las mismas imágenes, y deja abierto que fallen
|
| 112 |
+
prediciendo clases distintas. quien quiera afirmar «predice lo
|
| 113 |
+
mismo» necesita comparar el argmax.
|
| 114 |
+
|
| 115 |
+
args:
|
| 116 |
+
modelo: en eval, sobre `disp`.
|
| 117 |
+
loader: dataloader de validación, sin barajar.
|
| 118 |
+
disp: dispositivo.
|
| 119 |
+
|
| 120 |
+
returns:
|
| 121 |
+
(pred [n] int64 con la clase predicha, ok [n] bool).
|
| 122 |
+
"""
|
| 123 |
+
modelo.eval()
|
| 124 |
+
preds, oks = [], []
|
| 125 |
+
for imgs, lbls in loader:
|
| 126 |
+
pred = modelo(imgs.to(disp, non_blocking=True)).argmax(dim=1).cpu()
|
| 127 |
+
preds.append(pred)
|
| 128 |
+
oks.append(pred == lbls)
|
| 129 |
+
return torch.cat(preds), torch.cat(oks)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@torch.no_grad()
|
| 133 |
+
def aciertos_por_imagen(
|
| 134 |
+
modelo: torch.nn.Module,
|
| 135 |
+
loader: DataLoader,
|
| 136 |
+
disp: str = "cuda",
|
| 137 |
+
) -> torch.Tensor:
|
| 138 |
+
"""solo el vector de aciertos; envoltorio de la función anterior.
|
| 139 |
+
|
| 140 |
+
args:
|
| 141 |
+
modelo: en eval, sobre `disp`.
|
| 142 |
+
loader: dataloader de validación, sin barajar.
|
| 143 |
+
disp: dispositivo.
|
| 144 |
+
|
| 145 |
+
returns:
|
| 146 |
+
tensor bool [n], True donde la predicción acierta.
|
| 147 |
+
"""
|
| 148 |
+
return predicciones_y_aciertos(modelo, loader, disp)[1]
|
src/gauges.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""construcción de gauges valor-salida en fp64.
|
| 2 |
+
|
| 3 |
+
reutiliza la construcción del paper 1 (angular-separation-vit):
|
| 4 |
+
ortogonales vía qr de una gaussiana (haar), gl generales vía
|
| 5 |
+
randn + escala·identidad. toda la aritmética en float64 —la lección
|
| 6 |
+
del r casi-singular del paper 1 es constitutiva aquí, no una nota al
|
| 7 |
+
pie.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def ortogonal(
|
| 14 |
+
dim_cabeza: int,
|
| 15 |
+
generador: torch.Generator,
|
| 16 |
+
) -> torch.Tensor:
|
| 17 |
+
"""muestra una rotación haar-aleatoria en o(dim_cabeza), fp64.
|
| 18 |
+
|
| 19 |
+
args:
|
| 20 |
+
dim_cabeza: d_h, tamaño de la matriz cuadrada.
|
| 21 |
+
generador: generador de torch para reproducibilidad.
|
| 22 |
+
|
| 23 |
+
returns:
|
| 24 |
+
tensor [d_h, d_h] ortogonal en float64.
|
| 25 |
+
"""
|
| 26 |
+
a = torch.randn(dim_cabeza, dim_cabeza, generator=generador,
|
| 27 |
+
dtype=torch.float64)
|
| 28 |
+
q, r = torch.linalg.qr(a)
|
| 29 |
+
# signo de la diagonal de r fija el signo de q (convención qr no
|
| 30 |
+
# es única si no se corrige); sin esto la distribución no es haar.
|
| 31 |
+
signos = torch.sign(torch.diagonal(r))
|
| 32 |
+
signos[signos == 0] = 1.0
|
| 33 |
+
return q * signos
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def gl_general(
|
| 37 |
+
dim_cabeza: int,
|
| 38 |
+
escala: float,
|
| 39 |
+
generador: torch.Generator,
|
| 40 |
+
) -> torch.Tensor:
|
| 41 |
+
"""muestra r = randn + escala·i en gl(dim_cabeza), fp64.
|
| 42 |
+
|
| 43 |
+
escala alta acerca r a un múltiplo escalar de la identidad (gauge
|
| 44 |
+
débil, bien condicionado); escala baja lo aleja (gauge fuerte,
|
| 45 |
+
condición alta, la cola que la nota espera que cuantice mal).
|
| 46 |
+
|
| 47 |
+
args:
|
| 48 |
+
dim_cabeza: d_h.
|
| 49 |
+
escala: coeficiente de la identidad.
|
| 50 |
+
generador: generador de torch para reproducibilidad.
|
| 51 |
+
|
| 52 |
+
returns:
|
| 53 |
+
tensor [d_h, d_h] en float64, invertible con probabilidad 1.
|
| 54 |
+
"""
|
| 55 |
+
ident = torch.eye(dim_cabeza, dtype=torch.float64)
|
| 56 |
+
ruido = torch.randn(dim_cabeza, dim_cabeza, generator=generador,
|
| 57 |
+
dtype=torch.float64)
|
| 58 |
+
return ruido + escala * ident
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def aplica_gauge(
|
| 62 |
+
w_v_h: torch.Tensor,
|
| 63 |
+
w_o_h: torch.Tensor,
|
| 64 |
+
r: torch.Tensor,
|
| 65 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 66 |
+
"""transforma (w_v^(h), w_o^(h)) bajo el gauge r, sin tocar el circuito.
|
| 67 |
+
|
| 68 |
+
convención: w_v^(h) es [d_h, d] (filas de valor), w_o^(h) es
|
| 69 |
+
[d, d_h] (columnas de salida). w_v <- r^t w_v, w_o <- w_o r^-t;
|
| 70 |
+
el circuito m = w_o w_v es invariante para cualquier r en
|
| 71 |
+
gl(d_h) (no solo ortogonal): m' = (w_o r^-t)(r^t w_v)
|
| 72 |
+
= w_o (r^-t r^t) w_v = w_o w_v, ya que r^-t r^t = (r^t)^-1 r^t = i.
|
| 73 |
+
|
| 74 |
+
args:
|
| 75 |
+
w_v_h: tensor [d_h, d] en float64.
|
| 76 |
+
w_o_h: tensor [d, d_h] en float64.
|
| 77 |
+
r: gauge [d_h, d_h] en float64, invertible.
|
| 78 |
+
|
| 79 |
+
returns:
|
| 80 |
+
(w_v_h transformado, w_o_h transformado), misma forma.
|
| 81 |
+
"""
|
| 82 |
+
r_inv_t = torch.linalg.inv(r).transpose(-2, -1)
|
| 83 |
+
return r.transpose(-2, -1) @ w_v_h, w_o_h @ r_inv_t
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def gl_condicionado(
|
| 87 |
+
dim_cabeza: int,
|
| 88 |
+
kappa: float,
|
| 89 |
+
generador: torch.Generator,
|
| 90 |
+
) -> torch.Tensor:
|
| 91 |
+
"""muestra r en gl(dim_cabeza) con número de condición kappa exacto.
|
| 92 |
+
|
| 93 |
+
se toma una gaussiana, se le extrae la svd y se sustituye su
|
| 94 |
+
espectro por uno geométrico entre 1 y kappa: las bases quedan
|
| 95 |
+
haar-aleatorias y el condicionamiento pasa a ser la variable
|
| 96 |
+
controlada, en vez de una consecuencia de la escala. la
|
| 97 |
+
alternativa declarada y no elegida es el espectro de un solo
|
| 98 |
+
outlier (sigma_1 = kappa, resto 1).
|
| 99 |
+
|
| 100 |
+
kappa = 1 devuelve una matriz ortogonal, que es el ancla del
|
| 101 |
+
suelo e_0 del ajuste.
|
| 102 |
+
|
| 103 |
+
args:
|
| 104 |
+
dim_cabeza: d_h.
|
| 105 |
+
kappa: número de condición objetivo, >= 1.
|
| 106 |
+
generador: generador de torch para reproducibilidad.
|
| 107 |
+
|
| 108 |
+
returns:
|
| 109 |
+
tensor [d_h, d_h] en float64 con cond(r) = kappa.
|
| 110 |
+
|
| 111 |
+
raises:
|
| 112 |
+
ValueError: si kappa < 1.
|
| 113 |
+
"""
|
| 114 |
+
if kappa < 1.0:
|
| 115 |
+
raise ValueError(f"kappa debe ser >= 1: {kappa}")
|
| 116 |
+
a = torch.randn(dim_cabeza, dim_cabeza, generator=generador,
|
| 117 |
+
dtype=torch.float64)
|
| 118 |
+
u, _, vh = torch.linalg.svd(a)
|
| 119 |
+
# espectro geométrico descendente de kappa a 1; el cociente entre
|
| 120 |
+
# el mayor y el menor es kappa por construcción.
|
| 121 |
+
exponentes = torch.linspace(1.0, 0.0, dim_cabeza, dtype=torch.float64)
|
| 122 |
+
s = torch.pow(torch.tensor(kappa, dtype=torch.float64), exponentes)
|
| 123 |
+
return (u * s) @ vh
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def producto_normas(
|
| 127 |
+
w_v_h: torch.Tensor,
|
| 128 |
+
w_o_h: torch.Tensor,
|
| 129 |
+
r: torch.Tensor | None = None,
|
| 130 |
+
) -> float:
|
| 131 |
+
"""p(r) = ||r^t w_v||_f ||w_o r^-t||_f / (||w_v||_f ||w_o||_f).
|
| 132 |
+
|
| 133 |
+
la variable de la ley del producto: el inflado del error de
|
| 134 |
+
cuantización a primer orden. vale 1 en la identidad y 1 en toda
|
| 135 |
+
la clase conforme ortogonal {c·q}, porque ||w_v (cq)|| escala por
|
| 136 |
+
c y ||(cq)^-1 w_o|| por 1/c.
|
| 137 |
+
|
| 138 |
+
args:
|
| 139 |
+
w_v_h: tensor [d_h, d] en float64, sin gauge.
|
| 140 |
+
w_o_h: tensor [d, d_h] en float64, sin gauge.
|
| 141 |
+
r: gauge [d_h, d_h]; None denota la identidad (p = 1).
|
| 142 |
+
|
| 143 |
+
returns:
|
| 144 |
+
p(r), escalar float.
|
| 145 |
+
"""
|
| 146 |
+
base = float(w_v_h.norm() * w_o_h.norm())
|
| 147 |
+
if r is None:
|
| 148 |
+
return 1.0
|
| 149 |
+
w_v_g, w_o_g = aplica_gauge(w_v_h, w_o_h, r)
|
| 150 |
+
return float(w_v_g.norm() * w_o_g.norm()) / base
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def gauge_balanceado(
|
| 154 |
+
w_v_h: torch.Tensor,
|
| 155 |
+
w_o_h: torch.Tensor,
|
| 156 |
+
tol_rango: float = 1e-10,
|
| 157 |
+
) -> tuple[torch.Tensor, float]:
|
| 158 |
+
"""el gauge que lleva el par a su factorización balanceada.
|
| 159 |
+
|
| 160 |
+
el punto balanceado del paper 1 (lema del punto balanceado)
|
| 161 |
+
minimiza ||w_v r||_f^2 + ||r^-1 w_o||_f^2 sobre la órbita a
|
| 162 |
+
circuito fijo, y se construye por svd del circuito: con
|
| 163 |
+
m = w_o w_v = u s v^t de rango d_h, el par balanceado es
|
| 164 |
+
a = u s^{1/2} y b = s^{1/2} v^t.
|
| 165 |
+
|
| 166 |
+
el enlace con esta nota es exacto: ||a||_f ||b||_f = tr(s) =
|
| 167 |
+
||m||_*, que es el mínimo del producto de normas sobre la órbita
|
| 168 |
+
entera (desigualdad de la norma nuclear, ||m||_* <= ||a||_f
|
| 169 |
+
||b||_f para toda factorización m = ab). el punto balanceado es
|
| 170 |
+
entonces el óptimo que la ley del producto predice, y es un gauge
|
| 171 |
+
NO ortogonal en general: el contraejemplo medido a «la
|
| 172 |
+
restricción ortogonal es necesaria».
|
| 173 |
+
|
| 174 |
+
convención de este repo: m = w_o w_v, y el gauge actúa como
|
| 175 |
+
w_v <- r^t w_v, w_o <- w_o r^-t. con s = r^t se pide
|
| 176 |
+
w_o s^-1 = a y s w_v = b.
|
| 177 |
+
|
| 178 |
+
args:
|
| 179 |
+
w_v_h: tensor [d_h, d] en float64.
|
| 180 |
+
w_o_h: tensor [d, d_h] en float64.
|
| 181 |
+
tol_rango: valor singular mínimo relativo para declarar el
|
| 182 |
+
circuito de rango completo.
|
| 183 |
+
|
| 184 |
+
returns:
|
| 185 |
+
(r [d_h, d_h] en float64, p_balanceado), donde p_balanceado
|
| 186 |
+
es ||m||_* / (||w_v||_f ||w_o||_f) <= 1, el valor de la ley
|
| 187 |
+
del producto en ese punto.
|
| 188 |
+
|
| 189 |
+
raises:
|
| 190 |
+
ValueError: si el circuito no tiene rango d_h, donde el punto
|
| 191 |
+
balanceado no es alcanzable por un gauge de gl(d_h).
|
| 192 |
+
"""
|
| 193 |
+
d_h = w_v_h.shape[0]
|
| 194 |
+
m = w_o_h @ w_v_h
|
| 195 |
+
u, s, vh = torch.linalg.svd(m, full_matrices=False)
|
| 196 |
+
u, s, vh = u[:, :d_h], s[:d_h], vh[:d_h]
|
| 197 |
+
if float(s[-1]) <= tol_rango * float(s[0]):
|
| 198 |
+
raise ValueError(
|
| 199 |
+
f"circuito de rango deficiente: s_min/s_max = "
|
| 200 |
+
f"{float(s[-1]) / float(s[0]):.2e} <= {tol_rango:.0e}")
|
| 201 |
+
raiz = s.sqrt()
|
| 202 |
+
a_bal = u * raiz # [d, d_h]
|
| 203 |
+
# s^-1 = w_o^+ a_bal; r = s^t = (s^-1)^-t
|
| 204 |
+
s_inv = torch.linalg.pinv(w_o_h) @ a_bal
|
| 205 |
+
r = torch.linalg.inv(s_inv).transpose(-2, -1)
|
| 206 |
+
p_bal = float(s.sum()) / float(w_v_h.norm() * w_o_h.norm())
|
| 207 |
+
return r, p_bal
|
src/metrics.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""error relativo del circuito ov, recompuesto siempre en fp64.
|
| 2 |
+
|
| 3 |
+
el circuito m = w_o^(h) w_v^(h) es la cantidad gauge-invariante (ver
|
| 4 |
+
paper 1, sec:gauge). la nota mide cuánto se aleja la versión
|
| 5 |
+
cuantizada-y-recompuesta de la referencia exacta.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def circuito(w_v_h: torch.Tensor, w_o_h: torch.Tensor) -> torch.Tensor:
|
| 12 |
+
"""circuito m = w_o^(h) w_v^(h), [d, d].
|
| 13 |
+
|
| 14 |
+
args:
|
| 15 |
+
w_v_h: tensor [d_h, d].
|
| 16 |
+
w_o_h: tensor [d, d_h].
|
| 17 |
+
|
| 18 |
+
returns:
|
| 19 |
+
tensor [d, d] en la dtype de entrada.
|
| 20 |
+
"""
|
| 21 |
+
return w_o_h @ w_v_h
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def error_relativo(
|
| 25 |
+
w_v_hat: torch.Tensor,
|
| 26 |
+
w_o_hat: torch.Tensor,
|
| 27 |
+
m_referencia: torch.Tensor,
|
| 28 |
+
) -> float:
|
| 29 |
+
"""e = ||m_hat - m_referencia||_f / ||m_referencia||_f, en fp64.
|
| 30 |
+
|
| 31 |
+
args:
|
| 32 |
+
w_v_hat: w_v^(h) tras gauge y cuantización, [d_h, d].
|
| 33 |
+
w_o_hat: w_o^(h) tras gauge y cuantización, [d, d_h].
|
| 34 |
+
m_referencia: circuito exacto de referencia (sin gauge, sin
|
| 35 |
+
cuantizar), [d, d], en float64.
|
| 36 |
+
|
| 37 |
+
returns:
|
| 38 |
+
error relativo de frobenius, escalar float.
|
| 39 |
+
"""
|
| 40 |
+
m_hat = circuito(w_v_hat.double(), w_o_hat.double())
|
| 41 |
+
return float(
|
| 42 |
+
(m_hat - m_referencia).norm() / m_referencia.norm())
|
src/quantizer.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""cuantizador rtn simétrico por canal, con des-cuantización.
|
| 2 |
+
|
| 3 |
+
round-to-nearest puro: sin gptq, sin rotaciones aprendidas, sin
|
| 4 |
+
calibración con datos. la simplicidad es deliberada —aísla la
|
| 5 |
+
variable de interés (el gauge), la nota no compite con métodos de
|
| 6 |
+
cuantización, los caracteriza (ver spec, sec. 2.2).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
_QMAX = {4: 7, 8: 127} # 2^(bits-1) - 1, rango simétrico con signo
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def rtn_cuantiza_descuantiza(
|
| 15 |
+
w: torch.Tensor,
|
| 16 |
+
bits: int,
|
| 17 |
+
) -> torch.Tensor:
|
| 18 |
+
"""cuantiza y descuantiza w por canal (fila), simétrico, rtn.
|
| 19 |
+
|
| 20 |
+
cada fila obtiene su propia escala —el "canal" es la fila, la
|
| 21 |
+
convención estándar para pesos [salida, entrada]—. la aritmética
|
| 22 |
+
es en la dtype de entrada (se espera float64, el suelo numérico
|
| 23 |
+
del resto de la nota); no hay almacenamiento entero real, se
|
| 24 |
+
simula la rejilla de cuantización.
|
| 25 |
+
|
| 26 |
+
args:
|
| 27 |
+
w: tensor [filas, columnas].
|
| 28 |
+
bits: 4 u 8.
|
| 29 |
+
|
| 30 |
+
returns:
|
| 31 |
+
tensor de la misma forma y dtype, redondeado a la rejilla de
|
| 32 |
+
`bits` y devuelto a escala real (des-cuantizado).
|
| 33 |
+
"""
|
| 34 |
+
if bits not in _QMAX:
|
| 35 |
+
raise ValueError(f"bits no soportado: {bits}")
|
| 36 |
+
qmax = _QMAX[bits]
|
| 37 |
+
escala = w.abs().amax(dim=-1, keepdim=True) / qmax
|
| 38 |
+
# fila nula: escala 0/0 -> nan; se fija a 1 (la fila cuantizada es
|
| 39 |
+
# 0 de todas formas, la escala no importa).
|
| 40 |
+
escala = torch.where(escala > 0, escala, torch.ones_like(escala))
|
| 41 |
+
q = torch.round(w / escala).clamp(min=-qmax, max=qmax)
|
| 42 |
+
return q * escala
|
src/reconstruccion.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""reconstruye un gauge concreto del barrido, re-jugando la misma
|
| 2 |
+
secuencia de muestreo que ``scripts/sweep.py::barre_capa`` con el
|
| 3 |
+
mismo generador y semilla. necesario para la validación end-to-end
|
| 4 |
+
(sec. 2.4 del spec): identifica el mejor/peor gauge MUESTREADO por el
|
| 5 |
+
barrido y lo reconstruye exactamente, sin persistir cada r en el csv.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from src.gauges import gl_general, ortogonal
|
| 11 |
+
|
| 12 |
+
K_MUESTRAS = 32
|
| 13 |
+
ESCALAS_GL = [2.0, 8.0, 32.0]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def reconstruye_r(
|
| 17 |
+
capa_objetivo: int,
|
| 18 |
+
cabeza_objetivo: int,
|
| 19 |
+
regimen_objetivo: str,
|
| 20 |
+
muestra_objetivo: int,
|
| 21 |
+
n_cabezas: int,
|
| 22 |
+
dim_cabeza: int,
|
| 23 |
+
n_capas_previas_por_capa: dict[int, int] | None = None,
|
| 24 |
+
semilla: int = 0,
|
| 25 |
+
escala_objetivo: float | None = None,
|
| 26 |
+
) -> torch.Tensor:
|
| 27 |
+
"""re-juega el muestreo hasta dar con el r exacto pedido.
|
| 28 |
+
|
| 29 |
+
reproduce el orden de ``barre_capa``: por capa, primero los k
|
| 30 |
+
gauges compartidos (orto_comp), luego por cabeza los k orto_ph
|
| 31 |
+
seguidos de los k·3 gl_e (por escala). se recorre capa a capa
|
| 32 |
+
desde 0 hasta la objetivo, consumiendo el generador exactamente
|
| 33 |
+
como el barrido original, y se descartan los r que no hacen falta.
|
| 34 |
+
|
| 35 |
+
args:
|
| 36 |
+
capa_objetivo: capa del gauge buscado.
|
| 37 |
+
cabeza_objetivo: cabeza del gauge buscado (ignorada si
|
| 38 |
+
regimen_objetivo es 'orto_comp', que es por-capa).
|
| 39 |
+
regimen_objetivo: 'orto_ph', 'orto_comp' o 'gl_e'.
|
| 40 |
+
muestra_objetivo: índice de muestra (0..31).
|
| 41 |
+
n_cabezas: cabezas por capa del modelo (constante).
|
| 42 |
+
dim_cabeza: d_h.
|
| 43 |
+
n_capas_previas_por_capa: sin uso (todas las capas del modelo
|
| 44 |
+
tienen el mismo n_cabezas); se mantiene por si algún
|
| 45 |
+
modelo futuro varía cabezas por capa.
|
| 46 |
+
semilla: semilla global del barrido (por defecto la del
|
| 47 |
+
barrido real, ver scripts/sweep.py --semilla).
|
| 48 |
+
escala_objetivo: escala de gl_e buscada (solo si
|
| 49 |
+
regimen_objetivo == 'gl_e').
|
| 50 |
+
|
| 51 |
+
returns:
|
| 52 |
+
tensor [d_h, d_h] en float64, idéntico al usado en el barrido.
|
| 53 |
+
"""
|
| 54 |
+
gen = torch.Generator().manual_seed(semilla)
|
| 55 |
+
for capa in range(capa_objetivo + 1):
|
| 56 |
+
compartidos = [ortogonal(dim_cabeza, gen)
|
| 57 |
+
for _ in range(K_MUESTRAS)]
|
| 58 |
+
if capa == capa_objetivo and regimen_objetivo == "orto_comp":
|
| 59 |
+
return compartidos[muestra_objetivo]
|
| 60 |
+
ultima_capa = capa == capa_objetivo
|
| 61 |
+
for h in range(n_cabezas):
|
| 62 |
+
orto_ph = [ortogonal(dim_cabeza, gen)
|
| 63 |
+
for _ in range(K_MUESTRAS)]
|
| 64 |
+
if (ultima_capa and h == cabeza_objetivo
|
| 65 |
+
and regimen_objetivo == "orto_ph"):
|
| 66 |
+
return orto_ph[muestra_objetivo]
|
| 67 |
+
for escala in ESCALAS_GL:
|
| 68 |
+
gl = [gl_general(dim_cabeza, escala, gen)
|
| 69 |
+
for _ in range(K_MUESTRAS)]
|
| 70 |
+
if (ultima_capa and h == cabeza_objetivo
|
| 71 |
+
and regimen_objetivo == "gl_e"
|
| 72 |
+
and escala == escala_objetivo):
|
| 73 |
+
return gl[muestra_objetivo]
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"no se encontró capa={capa_objetivo} cabeza={cabeza_objetivo} "
|
| 76 |
+
f"regimen={regimen_objetivo} muestra={muestra_objetivo}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def reconstruye_muchos(
|
| 80 |
+
objetivos: set[tuple],
|
| 81 |
+
n_capas: int,
|
| 82 |
+
n_cabezas: int,
|
| 83 |
+
dim_cabeza: int,
|
| 84 |
+
semilla: int = 0,
|
| 85 |
+
) -> dict[tuple, torch.Tensor]:
|
| 86 |
+
"""como `reconstruye_r`, pero en una sola pasada para muchos r's.
|
| 87 |
+
|
| 88 |
+
evita rejugar la secuencia desde cero por cada objetivo (o(n) en
|
| 89 |
+
vez de o(n²)): recorre el modelo entero una vez y captura cada r
|
| 90 |
+
pedido al pasar por su punto exacto.
|
| 91 |
+
|
| 92 |
+
args:
|
| 93 |
+
objetivos: conjunto de tuplas
|
| 94 |
+
(capa, cabeza, regimen, muestra, escala) — escala es
|
| 95 |
+
``None`` salvo para regimen='gl_e'. en 'orto_comp' la r es
|
| 96 |
+
por capa y la misma para todas las cabezas, pero la clave
|
| 97 |
+
exige el índice real de la cabeza: se registra una entrada
|
| 98 |
+
por cabeza, no un comodín.
|
| 99 |
+
n_capas: capas totales del modelo (recorre 0..n_capas-1).
|
| 100 |
+
n_cabezas: cabezas por capa.
|
| 101 |
+
dim_cabeza: d_h.
|
| 102 |
+
semilla: semilla global del barrido.
|
| 103 |
+
|
| 104 |
+
returns:
|
| 105 |
+
dict objetivo -> tensor [d_h, d_h] en float64, con todas las
|
| 106 |
+
claves de `objetivos` presentes (lanza si falta alguna).
|
| 107 |
+
"""
|
| 108 |
+
restantes = set(objetivos)
|
| 109 |
+
hallados: dict[tuple, torch.Tensor] = {}
|
| 110 |
+
gen = torch.Generator().manual_seed(semilla)
|
| 111 |
+
for capa in range(n_capas):
|
| 112 |
+
compartidos = [ortogonal(dim_cabeza, gen)
|
| 113 |
+
for _ in range(K_MUESTRAS)]
|
| 114 |
+
for m, r in enumerate(compartidos):
|
| 115 |
+
for cab in range(n_cabezas):
|
| 116 |
+
clave = (capa, cab, "orto_comp", m, None)
|
| 117 |
+
if clave in restantes:
|
| 118 |
+
hallados[clave] = r
|
| 119 |
+
for h in range(n_cabezas):
|
| 120 |
+
orto_ph = [ortogonal(dim_cabeza, gen)
|
| 121 |
+
for _ in range(K_MUESTRAS)]
|
| 122 |
+
for m, r in enumerate(orto_ph):
|
| 123 |
+
clave = (capa, h, "orto_ph", m, None)
|
| 124 |
+
if clave in restantes:
|
| 125 |
+
hallados[clave] = r
|
| 126 |
+
for escala in ESCALAS_GL:
|
| 127 |
+
gl = [gl_general(dim_cabeza, escala, gen)
|
| 128 |
+
for _ in range(K_MUESTRAS)]
|
| 129 |
+
for m, r in enumerate(gl):
|
| 130 |
+
clave = (capa, h, "gl_e", m, escala)
|
| 131 |
+
if clave in restantes:
|
| 132 |
+
hallados[clave] = r
|
| 133 |
+
faltan = restantes - set(hallados)
|
| 134 |
+
if faltan:
|
| 135 |
+
raise ValueError(f"objetivos no encontrados: {faltan}")
|
| 136 |
+
return hallados
|
src/weights.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""carga de modelos y extracción por-cabeza de (w_v, w_o).
|
| 2 |
+
|
| 3 |
+
reimplementación autocontenida, para este repo, de la extracción ya
|
| 4 |
+
verificada en el paper 1 (angular-separation-vit): gpt-neox fusiona
|
| 5 |
+
q,k,v por cabeza intercalado (``query_key_value``); timm concatena
|
| 6 |
+
[q,k,v] por bloque y v ocupa el último tercio (``qkv``). convención
|
| 7 |
+
de forma en todo el repo: w_v^(h) es [d_h, d], w_o^(h) es [d, d_h].
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def cargar_pythia(
|
| 14 |
+
model_id: str = "EleutherAI/pythia-410m",
|
| 15 |
+
dtype: torch.dtype = torch.float32,
|
| 16 |
+
):
|
| 17 |
+
"""carga pythia-410m y verifica su anatomía contra el config real.
|
| 18 |
+
|
| 19 |
+
args:
|
| 20 |
+
model_id: identificador del modelo en hugging face hub.
|
| 21 |
+
dtype: precisión de carga.
|
| 22 |
+
|
| 23 |
+
returns:
|
| 24 |
+
el modelo en modo evaluación.
|
| 25 |
+
"""
|
| 26 |
+
from transformers import AutoModelForCausalLM
|
| 27 |
+
|
| 28 |
+
modelo = AutoModelForCausalLM.from_pretrained(
|
| 29 |
+
model_id, dtype=dtype, attn_implementation="eager")
|
| 30 |
+
modelo.eval()
|
| 31 |
+
cfg = modelo.config
|
| 32 |
+
dim_cabeza = cfg.hidden_size // cfg.num_attention_heads
|
| 33 |
+
assert dim_cabeza == 64, f"dim_cabeza inesperada: {dim_cabeza}"
|
| 34 |
+
assert cfg.num_attention_heads == 16, "num_attention_heads != 16"
|
| 35 |
+
assert cfg.hidden_size == 1024, "hidden_size != 1024"
|
| 36 |
+
assert len(modelo.gpt_neox.layers) == 24, "num capas != 24"
|
| 37 |
+
return modelo
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@torch.no_grad()
|
| 41 |
+
def w_v_w_o_pythia(
|
| 42 |
+
modelo,
|
| 43 |
+
capa: int,
|
| 44 |
+
n_cabezas: int = 16,
|
| 45 |
+
dim_cabeza: int = 64,
|
| 46 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 47 |
+
"""(w_v, w_o) por cabeza de una capa de pythia, en float64.
|
| 48 |
+
|
| 49 |
+
query_key_value fusiona q, k y v por cabeza: cada cabeza ocupa
|
| 50 |
+
``3*dim_cabeza`` filas contiguas ``[q_h|k_h|v_h]``; v_h es el
|
| 51 |
+
último tercio. dense es la proyección de salida (columnas por
|
| 52 |
+
cabeza).
|
| 53 |
+
|
| 54 |
+
args:
|
| 55 |
+
modelo: modelo gpt-neox (pythia) cargado.
|
| 56 |
+
capa: índice de capa.
|
| 57 |
+
n_cabezas: cabezas h.
|
| 58 |
+
dim_cabeza: d_h.
|
| 59 |
+
|
| 60 |
+
returns:
|
| 61 |
+
(w_v [h, d_h, d], w_o [h, d, d_h]), float64.
|
| 62 |
+
"""
|
| 63 |
+
attn = modelo.gpt_neox.layers[capa].attention
|
| 64 |
+
w_qkv = attn.query_key_value.weight # [3d, d]
|
| 65 |
+
w_dense = attn.dense.weight # [d, d]
|
| 66 |
+
w_v, w_o = [], []
|
| 67 |
+
for h in range(n_cabezas):
|
| 68 |
+
base = h * 3 * dim_cabeza + 2 * dim_cabeza
|
| 69 |
+
w_v.append(w_qkv[base:base + dim_cabeza, :].double())
|
| 70 |
+
w_o.append(w_dense[:, h * dim_cabeza:(h + 1) * dim_cabeza]
|
| 71 |
+
.double())
|
| 72 |
+
return torch.stack(w_v), torch.stack(w_o)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def cargar_vitb(
|
| 76 |
+
ckpt_path: str,
|
| 77 |
+
model_name: str = "vit_base_patch16_224",
|
| 78 |
+
num_classes: int = 100,
|
| 79 |
+
img_size: int = 224,
|
| 80 |
+
):
|
| 81 |
+
"""carga la columna vit-b con los pesos de un checkpoint de a.
|
| 82 |
+
|
| 83 |
+
args:
|
| 84 |
+
ckpt_path: ruta al checkpoint (.pt con clave 'model',
|
| 85 |
+
prefijo 'backbone.' en las claves).
|
| 86 |
+
model_name: identificador timm de la columna.
|
| 87 |
+
num_classes: clases de la cabeza.
|
| 88 |
+
img_size: resolución de entrada.
|
| 89 |
+
|
| 90 |
+
returns:
|
| 91 |
+
el vit interno de timm (el que expone .blocks), en eval.
|
| 92 |
+
"""
|
| 93 |
+
import timm
|
| 94 |
+
|
| 95 |
+
modelo = timm.create_model(
|
| 96 |
+
model_name, pretrained=False, num_classes=num_classes,
|
| 97 |
+
img_size=img_size)
|
| 98 |
+
blob = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 99 |
+
# el checkpoint viene de un wrapper HeadProjections del paper 1:
|
| 100 |
+
# las claves llevan 'backbone.model.' antes de la state_dict plana
|
| 101 |
+
# de timm ('backbone.' del checkpoint de entrenamiento, 'model.'
|
| 102 |
+
# del atributo submódulo del wrapper).
|
| 103 |
+
sd = {k.replace("backbone.model.", "", 1): v
|
| 104 |
+
for k, v in blob["model"].items()}
|
| 105 |
+
modelo.load_state_dict(sd)
|
| 106 |
+
return modelo.eval()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@torch.no_grad()
|
| 110 |
+
def w_v_w_o_vitb(
|
| 111 |
+
modelo,
|
| 112 |
+
capa: int,
|
| 113 |
+
n_cabezas: int = 12,
|
| 114 |
+
dim_cabeza: int = 64,
|
| 115 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 116 |
+
"""(w_v, w_o) por cabeza de una capa de vit-b, en float64.
|
| 117 |
+
|
| 118 |
+
en timm el qkv concatena [q, k, v] por bloque; v ocupa el último
|
| 119 |
+
tercio y se reordena por cabeza. proj es la proyección de salida.
|
| 120 |
+
|
| 121 |
+
args:
|
| 122 |
+
modelo: el vit interno de timm.
|
| 123 |
+
capa: índice de capa.
|
| 124 |
+
n_cabezas: cabezas h.
|
| 125 |
+
dim_cabeza: d_h.
|
| 126 |
+
|
| 127 |
+
returns:
|
| 128 |
+
(w_v [h, d_h, d], w_o [h, d, d_h]), float64.
|
| 129 |
+
"""
|
| 130 |
+
attn = modelo.blocks[capa].attn
|
| 131 |
+
w_qkv = attn.qkv.weight # [3d, d]
|
| 132 |
+
w_proj = attn.proj.weight # [d, d]
|
| 133 |
+
base_v = 2 * w_qkv.shape[1]
|
| 134 |
+
w_v, w_o = [], []
|
| 135 |
+
for h in range(n_cabezas):
|
| 136 |
+
fil = slice(base_v + h * dim_cabeza, base_v + (h + 1) * dim_cabeza)
|
| 137 |
+
col = slice(h * dim_cabeza, (h + 1) * dim_cabeza)
|
| 138 |
+
w_v.append(w_qkv[fil, :].double())
|
| 139 |
+
w_o.append(w_proj[:, col].double())
|
| 140 |
+
return torch.stack(w_v), torch.stack(w_o)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@torch.no_grad()
|
| 144 |
+
def escribe_v_o_vitb(
|
| 145 |
+
modelo,
|
| 146 |
+
capa: int,
|
| 147 |
+
h: int,
|
| 148 |
+
w_v_h: torch.Tensor,
|
| 149 |
+
w_o_h: torch.Tensor,
|
| 150 |
+
dim_cabeza: int = 64,
|
| 151 |
+
) -> None:
|
| 152 |
+
"""escribe (w_v, w_o) de una cabeza de vuelta en el modelo, in situ.
|
| 153 |
+
|
| 154 |
+
inversa de `w_v_w_o_vitb` para una sola cabeza: usada por la
|
| 155 |
+
validación end-to-end para materializar un modelo con los pesos
|
| 156 |
+
ya gaugeados-y-cuantizados antes del forward real.
|
| 157 |
+
|
| 158 |
+
args:
|
| 159 |
+
modelo: el vit interno de timm, modificado in situ.
|
| 160 |
+
capa: índice de capa.
|
| 161 |
+
h: índice de cabeza.
|
| 162 |
+
w_v_h: tensor [d_h, d] (cualquier dtype, se castea al del
|
| 163 |
+
modelo).
|
| 164 |
+
w_o_h: tensor [d, d_h].
|
| 165 |
+
dim_cabeza: d_h.
|
| 166 |
+
"""
|
| 167 |
+
attn = modelo.blocks[capa].attn
|
| 168 |
+
w_qkv = attn.qkv.weight
|
| 169 |
+
w_proj = attn.proj.weight
|
| 170 |
+
base_v = 2 * w_qkv.shape[1]
|
| 171 |
+
fil = slice(base_v + h * dim_cabeza, base_v + (h + 1) * dim_cabeza)
|
| 172 |
+
col = slice(h * dim_cabeza, (h + 1) * dim_cabeza)
|
| 173 |
+
w_qkv[fil, :] = w_v_h.to(w_qkv.dtype)
|
| 174 |
+
w_proj[:, col] = w_o_h.to(w_proj.dtype)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@torch.no_grad()
|
| 178 |
+
def b_v_vitb(
|
| 179 |
+
modelo,
|
| 180 |
+
capa: int,
|
| 181 |
+
n_cabezas: int = 12,
|
| 182 |
+
dim_cabeza: int = 64,
|
| 183 |
+
) -> torch.Tensor | None:
|
| 184 |
+
"""sesgo de valor por cabeza de una capa de vit-b, en float64.
|
| 185 |
+
|
| 186 |
+
el sesgo de valor entra en la invariancia de gauge: como
|
| 187 |
+
v = x w_v^t + b_v, la compensación de w_o exige que b_v gire igual
|
| 188 |
+
que la weight (b_v <- r^t b_v). omitirlo en una columna con
|
| 189 |
+
qkv_bias rompe la invariancia, y el forward deja de medir el
|
| 190 |
+
gauge para medir otro modelo.
|
| 191 |
+
|
| 192 |
+
args:
|
| 193 |
+
modelo: el vit interno de timm.
|
| 194 |
+
capa: índice de capa.
|
| 195 |
+
n_cabezas: cabezas h.
|
| 196 |
+
dim_cabeza: d_h.
|
| 197 |
+
|
| 198 |
+
returns:
|
| 199 |
+
tensor [h, d_h] en float64, o None si la columna no lleva
|
| 200 |
+
qkv_bias.
|
| 201 |
+
"""
|
| 202 |
+
attn = modelo.blocks[capa].attn
|
| 203 |
+
if attn.qkv.bias is None:
|
| 204 |
+
return None
|
| 205 |
+
b_qkv = attn.qkv.bias
|
| 206 |
+
base_v = 2 * (b_qkv.shape[0] // 3)
|
| 207 |
+
return torch.stack([
|
| 208 |
+
b_qkv[base_v + h * dim_cabeza:
|
| 209 |
+
base_v + (h + 1) * dim_cabeza].double()
|
| 210 |
+
for h in range(n_cabezas)])
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@torch.no_grad()
|
| 214 |
+
def escribe_b_v_vitb(
|
| 215 |
+
modelo,
|
| 216 |
+
capa: int,
|
| 217 |
+
h: int,
|
| 218 |
+
b_v_h: torch.Tensor,
|
| 219 |
+
dim_cabeza: int = 64,
|
| 220 |
+
) -> None:
|
| 221 |
+
"""escribe el sesgo de valor de una cabeza, in situ.
|
| 222 |
+
|
| 223 |
+
inversa de `b_v_vitb` para una sola cabeza. el sesgo no se
|
| 224 |
+
cuantiza: el objeto cuantizado de la nota es el par de weights
|
| 225 |
+
(w_v, w_o), y el sesgo solo viaja para que el gauge siga siendo
|
| 226 |
+
un cambio de coordenadas y no un cambio de función.
|
| 227 |
+
|
| 228 |
+
args:
|
| 229 |
+
modelo: el vit interno de timm, modificado in situ.
|
| 230 |
+
capa: índice de capa.
|
| 231 |
+
h: índice de cabeza.
|
| 232 |
+
b_v_h: tensor [d_h], se castea al dtype del modelo.
|
| 233 |
+
dim_cabeza: d_h.
|
| 234 |
+
"""
|
| 235 |
+
attn = modelo.blocks[capa].attn
|
| 236 |
+
if attn.qkv.bias is None:
|
| 237 |
+
raise ValueError("la columna no lleva qkv_bias")
|
| 238 |
+
b_qkv = attn.qkv.bias
|
| 239 |
+
base_v = 2 * (b_qkv.shape[0] // 3)
|
| 240 |
+
fil = slice(base_v + h * dim_cabeza, base_v + (h + 1) * dim_cabeza)
|
| 241 |
+
b_qkv[fil] = b_v_h.to(b_qkv.dtype)
|