Phase 6.4: make the gates fail when they cannot verify something
A code review found seven ways these gates could pass green with something actually wrong. All are the same family: a missing value was treated as OK. The rule now written into all three files is that absent is not OK, absent is "could not verify", and that either fails or is reported as an explicit SKIP - it never slips through as green. 30_eval_suite.py: - A bucket with no baseline of its own fell back to the global 0.2750 and printed it in a column headed "baseline", as if it were that bucket's number. Measured against the real eval.jsonl buckets: negativos going from 0.12 to 0.33 is a real +0.21 regression, but the computed delta was +0.055 and it PASSED; manejo_errores sitting unchanged at 0.42 produced a fabricated +0.145 FAIL that would have discarded a healthy candidate mid-downtime. Now such buckets print SKIP and the verdict reports how many went unverified. - "VEREDICTO: FAIL" exited 0, so a runbook chaining the gate into quantization would have carried on to write 24 GB. Now exits 1. - A typo in BASELINE_BUCKET_LOSSES silently matched nothing; now aborts. - The penpot exemption is labelled honestly: those 11 rows are pre-existing LoRA #1 tool-calling, not new capability, so gate 1 has no regression coverage there and the log says so. 20_merge_lora.py dry-run (merge path untouched, verified by AST diff): - adapter_config.get("use_rslora", False) meant a missing key passed AND the log printed use_rslora=False, asserting it had checked something that was never there. A different PEFT version omitting a key was enough. - lora_bias was not checked at all, only bias. They are different fields: lora_bias puts a bias inside lora_B, which W + scaling * (B @ A) ignores. - The 620 keys were printed but never asserted, so an adapter with extra tensors printed "310 + 310 = 930" and passed. - rank_pattern/alpha_pattern were not checked. They set r per module, so scaling is not uniformly alpha/r while both the dry-run and the merge apply a single 2.0 to all 310 tensors. - A missing family was invisible: swap linear_attn for 150 mlp.gate targets and the total is still 310, no norm is zero because the family is simply gone, and it passed. Now presence and per-family counts are asserted, derived from the real adapter: linear_attn 150, shared_expert 120, attention_qkvo 40, otros 0. Verified against seven synthetic adapters plus the real phase 3 one; only the correct adapter passes. 21_quantize_nvfp4.py (recipe and oneshot untouched): the calibration cache now carries a provenance.json recording the training file's sha256, the recipe numbers and the bucket distribution, and loading aborts on mismatch. This is the phase's number one risk and it had no mechanical defence: the phase 5 cache on disk has exactly 512 rows, the same as the v2 recipe, so the only existing check could not tell them apart and reusing it would have calibrated with zero design data and washed out the new capability silently. Verified: that cache now aborts. gate 5: retry transport failures against the Penpot MCP, which drops connections mid-call intermittently (seen before in phase 4's gate 4). Without it a blip on prompt 6 of 8 kills a whole run and reads like a model failure. PluginNotConnected is deliberately not retried - that is a real state of the world. Also unwrap the {"result":..., "log":...} envelope the server wraps execute_code returns in; the gate was reading keys off the outer object and rejecting a valid page setup.
This commit is contained in:
@@ -13,7 +13,7 @@ una muestra de data/train.jsonl (el propio dataset de fine-tuning) en vez del
|
||||
corpus generico ultrachat_200k, aplicando el chat template de PRODUCCION (el que
|
||||
ya trae MODEL_PATH, no el de masking de training).
|
||||
|
||||
Soporta ademas NUM_ULTRACHAT_SAMPLES (default 0) para mezclar N muestras de
|
||||
Soporta ademas NUM_ULTRACHAT_SAMPLES (default 256) para mezclar N muestras de
|
||||
HuggingFaceH4/ultrachat_200k (split train_sft, el mismo corpus/split que uso
|
||||
RedHatAI) con (NUM_CALIBRATION_SAMPLES - NUM_ULTRACHAT_SAMPLES) muestras de
|
||||
TRAIN_DATA_PATH, concatenadas y mezcladas (shuffle, mismo seed=42) antes de
|
||||
@@ -62,7 +62,16 @@ confirmado empiricamente que hasta una corrida SIN ultrachat (NUM_CALIBRATION_SA
|
||||
mezcladas, en el mismo punto exacto del setup interno de oneshot()
|
||||
(disable_lm_head onload). Separar los dos procesos evita que la maquinaria de
|
||||
`datasets`/pyarrow/red conviva en el mismo proceso que el modelo cargado.
|
||||
|
||||
Al generar el cache se escribe adentro un sidecar provenance.json (ruta/mtime/sha256
|
||||
de TRAIN_DATA_PATH, los tres numeros de la receta y la distribucion por bucket), y al
|
||||
cargarlo se compara contra el env de la corrida, ABORTANDO si algo difiere. Es el
|
||||
riesgo #1 de la fase convertido en asercion: el chequeo anterior (solo el conteo de
|
||||
filas) no distinguia el cache de Fase 5 -- que tiene exactamente 512 filas, igual que
|
||||
la receta v2 -- de uno recien generado. Un cache SIN provenance.json tampoco pasa:
|
||||
ausente no es OK, es "no se pudo verificar de donde viene", y aborta igual.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
@@ -96,18 +105,32 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
MODEL_PATH = Path(os.environ.get("MODEL_PATH", "/workspace/ft-models/Qwen3.6-35B-A3B-mcp-bf16"))
|
||||
OUTPUT_PATH = Path(os.environ.get("OUTPUT_PATH", "/workspace/ft-models/Qwen3.6-35B-A3B-mcp-NVFP4"))
|
||||
TRAIN_DATA_PATH = Path(os.environ.get("TRAIN_DATA_PATH", str(REPO_ROOT / "data" / "train.jsonl")))
|
||||
NUM_CALIBRATION_SAMPLES = int(os.environ.get("NUM_CALIBRATION_SAMPLES", "256"))
|
||||
# RECETA DE PRODUCCION -- los defaults de abajo (512 total = 256 de TRAIN_DATA_PATH
|
||||
# + 256 de ultrachat, MAX_SEQUENCE_LENGTH=2048) son los que produjeron el checkpoint
|
||||
# NVFP4 que HOY esta en produccion. La prueba esta en el log de esa corrida en spark:
|
||||
#
|
||||
# /home/aleleba/ft-models/quantize_nvfp4_v6_mixed_2048.log
|
||||
#
|
||||
# Los defaults ORIGINALES de este script eran 256 / 0 / 8192, que fueron el PRIMER
|
||||
# INTENTO y REGRESARON en calidad (puertas 2-3). Se cambiaron a los de produccion
|
||||
# justamente para que una corrida pelada no vuelva a pisar esa trampa. Cualquier
|
||||
# cambio aca requiere volver a correr las puertas 2/3/4 contra el resultado.
|
||||
NUM_CALIBRATION_SAMPLES = int(os.environ.get("NUM_CALIBRATION_SAMPLES", "512"))
|
||||
# Muestras adicionales de un corpus generico y amplio (mismo dataset/split que uso
|
||||
# RedHatAI en su receta de referencia), mezcladas con las de TRAIN_DATA_PATH.
|
||||
# Hipotesis a probar: la regresion de calidad no es por CANTIDAD de muestras sino
|
||||
# por DIVERSIDAD -- calibrar solo con conversaciones angostas de los 5 MCPs/skills
|
||||
# del proyecto podria dejar a los 256 expertos MoE con una vision demasiado
|
||||
# estrecha. NUM_CALIBRATION_SAMPLES sigue siendo el TOTAL; la porcion de
|
||||
# TRAIN_DATA_PATH se reduce a (NUM_CALIBRATION_SAMPLES - NUM_ULTRACHAT_SAMPLES).
|
||||
NUM_ULTRACHAT_SAMPLES = int(os.environ.get("NUM_ULTRACHAT_SAMPLES", "0"))
|
||||
# Hipotesis probada y CONFIRMADA en quantize_nvfp4_v6_mixed_2048.log: la regresion
|
||||
# de calidad no era por CANTIDAD de muestras sino por DIVERSIDAD -- calibrar solo con
|
||||
# conversaciones angostas de los 5 MCPs/skills del proyecto dejaba a los 256 expertos
|
||||
# MoE con una vision demasiado estrecha. NUM_CALIBRATION_SAMPLES sigue siendo el
|
||||
# TOTAL; la porcion de TRAIN_DATA_PATH se reduce a
|
||||
# (NUM_CALIBRATION_SAMPLES - NUM_ULTRACHAT_SAMPLES), o sea 512 - 256 = 256.
|
||||
NUM_ULTRACHAT_SAMPLES = int(os.environ.get("NUM_ULTRACHAT_SAMPLES", "256"))
|
||||
ULTRACHAT_DATASET = "HuggingFaceH4/ultrachat_200k"
|
||||
ULTRACHAT_SPLIT = "train_sft"
|
||||
MAX_SEQUENCE_LENGTH = int(os.environ.get("MAX_SEQUENCE_LENGTH", "8192"))
|
||||
# 2048 y no 8192: ver quantize_nvfp4_v6_mixed_2048.log. Truncar mas corto permite
|
||||
# entrar 512 muestras en el presupuesto de memoria del pool unificado del GB10, y la
|
||||
# calibracion se beneficia mas de mas muestras diversas que de secuencias largas.
|
||||
MAX_SEQUENCE_LENGTH = int(os.environ.get("MAX_SEQUENCE_LENGTH", "2048"))
|
||||
CALIBRATION_SEED = 42
|
||||
# Preparar la muestra de calibracion (que puede requerir descargar/streamear
|
||||
# ultrachat_200k via `datasets`/pyarrow/red) y cuantizar (que carga el modelo de
|
||||
@@ -119,10 +142,176 @@ CALIBRATION_SEED = 42
|
||||
# procesos: --prepare-calibration construye y guarda el dataset ya tokenizado SIN
|
||||
# cargar el modelo; la cuantizacion normal lo carga desde disco (sin volver a
|
||||
# tocar `datasets`/red) antes de cargar el modelo.
|
||||
#
|
||||
# #############################################################################
|
||||
# RIESGO #1 DE LA FASE 6 -- CACHE DE CALIBRACION DE OTRA FASE:
|
||||
#
|
||||
# El cache es un Dataset ya tokenizado, sin nada adentro que diga de que
|
||||
# TRAIN_DATA_PATH, de que receta ni de que fase salio; y el conteo de filas puede
|
||||
# coincidir por casualidad (el cache de Fase 5 tiene exactamente 512 filas, igual
|
||||
# que la receta de produccion v2), asi que el viejo chequeo de `len(dataset) !=
|
||||
# NUM_CALIBRATION_SAMPLES` dejaba pasar el reuso sin un solo warning: se calibraria
|
||||
# con CERO datos de diseno, lavando justo la capacidad nueva, y todas las
|
||||
# verificaciones internas pasarian igual.
|
||||
#
|
||||
# El cache de Fase 5 esta EN DISCO AHORA en la ruta por default:
|
||||
# /home/aleleba/ft-models/nvfp4_calibration_cache
|
||||
#
|
||||
# Por eso el cache ya NO se carga a ciegas: al GENERARLO se escribe adentro un
|
||||
# sidecar provenance.json (ruta/mtime/sha256 de TRAIN_DATA_PATH, los tres numeros
|
||||
# de la receta y la distribucion por bucket) y al CARGARLO se compara contra el env
|
||||
# actual, abortando si algo difiere. Ausente tampoco es OK: un cache SIN
|
||||
# provenance.json (por ejemplo el de Fase 5) es "no se pudo verificar de donde
|
||||
# viene" y aborta igual -- hay que regenerarlo con --prepare-calibration.
|
||||
#
|
||||
# REGLA (sigue vigente, la verificacion es la red de seguridad, no el plan): cada
|
||||
# fase usa su PROPIA ruta de cache, por ejemplo
|
||||
# CALIBRATION_CACHE_PATH=/workspace/ft-models/nvfp4_calibration_cache_v2
|
||||
# y verifica en el log la linea "[CALIB]" que este script imprime SIEMPRE con la
|
||||
# ruta usada, si la cargo o la genero, el conteo de filas y la distribucion por
|
||||
# bucket.
|
||||
# #############################################################################
|
||||
CALIBRATION_CACHE_PATH = Path(
|
||||
os.environ.get("CALIBRATION_CACHE_PATH", "/workspace/ft-models/nvfp4_calibration_cache")
|
||||
)
|
||||
|
||||
# Nombre del sidecar de procedencia que se escribe DENTRO del directorio del cache.
|
||||
CALIBRATION_PROVENANCE_FILENAME = "provenance.json"
|
||||
|
||||
# Distribucion por bucket de la ultima muestra de TRAIN_DATA_PATH construida en
|
||||
# ESTE proceso. Solo para reportar; queda vacia cuando la muestra vino del cache
|
||||
# (un Dataset tokenizado no conserva meta.bucket) Y TAMBIEN cuando la receta no usa
|
||||
# TRAIN_DATA_PATH en absoluto (NUM_ULTRACHAT_SAMPLES == NUM_CALIBRATION_SAMPLES),
|
||||
# por eso el reporte se condiciona sobre el ORIGEN real y no sobre este dict vacio.
|
||||
LAST_TRAIN_BUCKET_COUNTS = {}
|
||||
|
||||
|
||||
def num_train_samples():
|
||||
"""Cuantas muestras salen de TRAIN_DATA_PATH con la receta actual (el resto es
|
||||
ultrachat). Cero significa que TRAIN_DATA_PATH no se toca en esta corrida."""
|
||||
return NUM_CALIBRATION_SAMPLES - NUM_ULTRACHAT_SAMPLES
|
||||
|
||||
|
||||
def train_data_fingerprint():
|
||||
"""Huella de TRAIN_DATA_PATH: ruta, mtime y sha256 del contenido. El sha256 es lo
|
||||
que realmente identifica el dataset (el mtime cambia con un `touch` o una copia)."""
|
||||
digest = hashlib.sha256()
|
||||
with open(TRAIN_DATA_PATH, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return {
|
||||
"path": str(TRAIN_DATA_PATH),
|
||||
"mtime": TRAIN_DATA_PATH.stat().st_mtime,
|
||||
"sha256": digest.hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def current_calibration_provenance(num_rows):
|
||||
"""Procedencia de la muestra construida en ESTE proceso, con la receta en uso."""
|
||||
num_train = num_train_samples()
|
||||
return {
|
||||
"train_data": train_data_fingerprint() if num_train > 0 else None,
|
||||
"recipe": {
|
||||
"NUM_CALIBRATION_SAMPLES": NUM_CALIBRATION_SAMPLES,
|
||||
"NUM_ULTRACHAT_SAMPLES": NUM_ULTRACHAT_SAMPLES,
|
||||
"MAX_SEQUENCE_LENGTH": MAX_SEQUENCE_LENGTH,
|
||||
},
|
||||
"num_train_samples": num_train,
|
||||
"calibration_seed": CALIBRATION_SEED,
|
||||
"ultrachat_dataset": ULTRACHAT_DATASET,
|
||||
"ultrachat_split": ULTRACHAT_SPLIT,
|
||||
"bucket_counts": dict(LAST_TRAIN_BUCKET_COUNTS),
|
||||
"num_rows": num_rows,
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
}
|
||||
|
||||
|
||||
def write_calibration_provenance(dataset):
|
||||
"""Escribe el sidecar de procedencia dentro del directorio del cache."""
|
||||
provenance = current_calibration_provenance(len(dataset))
|
||||
path = CALIBRATION_CACHE_PATH / CALIBRATION_PROVENANCE_FILENAME
|
||||
path.write_text(json.dumps(provenance, indent=2, sort_keys=True), encoding="utf-8")
|
||||
print(f"[CALIB] provenance.json escrito en {path}")
|
||||
return provenance
|
||||
|
||||
|
||||
def verify_calibration_provenance(dataset):
|
||||
"""Compara el sidecar del cache contra el env/receta de ESTA corrida y aborta si
|
||||
difieren. Convierte el riesgo #1 (hoy mitigado solo por un comentario) en una
|
||||
asercion: el unico chequeo anterior era len(dataset) != NUM_CALIBRATION_SAMPLES,
|
||||
y el cache de Fase 5 tiene exactamente 512 filas igual que la receta v2, o sea
|
||||
que reusarlo pasaba en verde.
|
||||
|
||||
Ausente no es OK: un cache sin provenance.json es 'no se pudo verificar de que
|
||||
fase viene', y eso aborta -- no se degrada a warning."""
|
||||
path = CALIBRATION_CACHE_PATH / CALIBRATION_PROVENANCE_FILENAME
|
||||
if not path.exists():
|
||||
raise AssertionError(
|
||||
f"el cache de calibracion {CALIBRATION_CACHE_PATH} no tiene "
|
||||
f"{CALIBRATION_PROVENANCE_FILENAME}: no se puede verificar con que datos ni con que "
|
||||
"receta fue construido (es un cache viejo, anterior a este chequeo -- probablemente "
|
||||
"el de Fase 5). Regenerarlo con --prepare-calibration en una ruta propia de esta fase"
|
||||
)
|
||||
provenance = json.loads(path.read_text(encoding="utf-8"))
|
||||
actual = current_calibration_provenance(len(dataset))
|
||||
|
||||
diffs = []
|
||||
for clave, esperado in actual["recipe"].items():
|
||||
del_cache = provenance.get("recipe", {}).get(clave, "<AUSENTE>")
|
||||
if del_cache != esperado:
|
||||
diffs.append(f"receta.{clave}: cache={del_cache!r} vs corrida actual={esperado!r}")
|
||||
if provenance.get("num_rows", "<AUSENTE>") != len(dataset):
|
||||
diffs.append(
|
||||
f"num_rows: provenance dice {provenance.get('num_rows', '<AUSENTE>')!r} pero el "
|
||||
f"Dataset en disco tiene {len(dataset)} filas (cache corrupto o pisado)"
|
||||
)
|
||||
if len(dataset) != NUM_CALIBRATION_SAMPLES:
|
||||
diffs.append(
|
||||
f"filas del cache={len(dataset)} vs NUM_CALIBRATION_SAMPLES={NUM_CALIBRATION_SAMPLES}"
|
||||
)
|
||||
if provenance.get("calibration_seed", "<AUSENTE>") != CALIBRATION_SEED:
|
||||
diffs.append(
|
||||
f"calibration_seed: cache={provenance.get('calibration_seed', '<AUSENTE>')!r} vs "
|
||||
f"corrida actual={CALIBRATION_SEED!r}"
|
||||
)
|
||||
|
||||
cache_train = provenance.get("train_data", "<AUSENTE>")
|
||||
actual_train = actual["train_data"]
|
||||
if cache_train == "<AUSENTE>":
|
||||
diffs.append("train_data ausente del provenance.json -- no se puede verificar el dataset de calibracion")
|
||||
elif (cache_train is None) != (actual_train is None):
|
||||
diffs.append(
|
||||
f"uso de TRAIN_DATA_PATH: cache={'ninguno (100% ultrachat)' if cache_train is None else cache_train.get('path')} "
|
||||
f"vs corrida actual={'ninguno (100% ultrachat)' if actual_train is None else actual_train['path']}"
|
||||
)
|
||||
elif actual_train is not None:
|
||||
if cache_train.get("path") != actual_train["path"]:
|
||||
diffs.append(
|
||||
f"TRAIN_DATA_PATH: cache={cache_train.get('path')!r} vs corrida actual={actual_train['path']!r}"
|
||||
)
|
||||
if cache_train.get("sha256") != actual_train["sha256"]:
|
||||
diffs.append(
|
||||
f"sha256 de {actual_train['path']}: cache={cache_train.get('sha256')} vs "
|
||||
f"archivo actual={actual_train['sha256']} -- el cache se construyo con OTRO contenido"
|
||||
)
|
||||
elif cache_train.get("mtime") != actual_train["mtime"]:
|
||||
# Mismo contenido, otro mtime: una copia o un touch. No invalida el cache.
|
||||
print(
|
||||
f"[CALIB] [WARN] mtime de {actual_train['path']} cambio "
|
||||
f"({cache_train.get('mtime')} -> {actual_train['mtime']}) pero el sha256 coincide "
|
||||
"-- mismo contenido, no invalida el cache"
|
||||
)
|
||||
|
||||
if diffs:
|
||||
raise AssertionError(
|
||||
f"el cache de calibracion en {CALIBRATION_CACHE_PATH} NO corresponde a esta corrida:\n - "
|
||||
+ "\n - ".join(diffs)
|
||||
+ f"\nRegenerarlo con --prepare-calibration y CALIBRATION_CACHE_PATH propio de esta fase. "
|
||||
f"(provenance generado el {provenance.get('generated_at', '?')})"
|
||||
)
|
||||
print("[CALIB] provenance.json del cache verificado contra la receta actual: coincide")
|
||||
return provenance
|
||||
|
||||
# Receta identica a la de RedHatAI/Qwen3.6-35B-A3B-NVFP4 (recipe.yaml leido por SSH,
|
||||
# citado integro en PLAN.md). Las capas Gated DeltaNet (linear_attn) quedan en BF16
|
||||
# a proposito -- el LoRA se entreno ahi, pero al no cuantizarse no se agrega perdida
|
||||
@@ -160,6 +349,8 @@ def load_train_examples(n):
|
||||
|
||||
bucket_counts = Counter(ex.get("meta", {}).get("bucket", "?") for ex in sampled)
|
||||
print(f"[INFO] distribucion de buckets (train.jsonl): {dict(bucket_counts)}")
|
||||
LAST_TRAIN_BUCKET_COUNTS.clear()
|
||||
LAST_TRAIN_BUCKET_COUNTS.update(bucket_counts)
|
||||
return sampled
|
||||
|
||||
|
||||
@@ -230,6 +421,53 @@ def load_calibration_dataset(tokenizer):
|
||||
return Dataset.from_dict({"input_ids": input_ids_list, "attention_mask": attention_mask_list})
|
||||
|
||||
|
||||
def report_calibration_source(dataset, source, from_cache=False, provenance=None):
|
||||
"""Reporte [CALIB] -- se imprime SIEMPRE, en los dos caminos (cache o construida
|
||||
en el proceso): la ruta queda escrita en el log de la corrida, junto al conteo de
|
||||
filas y a la distribucion por bucket, para poder auditarlo despues. La defensa
|
||||
dura contra el riesgo #1 es verify_calibration_provenance(); esto es el rastro.
|
||||
|
||||
El origen se pasa EXPLICITO (from_cache) y no se deduce de que
|
||||
LAST_TRAIN_BUCKET_COUNTS este vacio: ese dict tambien queda vacio cuando la
|
||||
receta no usa TRAIN_DATA_PATH (NUM_ULTRACHAT_SAMPLES == NUM_CALIBRATION_SAMPLES),
|
||||
y entonces el mensaje mentia diciendo que la muestra habia venido del cache."""
|
||||
num_train = num_train_samples()
|
||||
print("[CALIB] ===== muestra de calibracion =====")
|
||||
print(f"[CALIB] CALIBRATION_CACHE_PATH = {CALIBRATION_CACHE_PATH}")
|
||||
print(f"[CALIB] origen = {source}")
|
||||
print(f"[CALIB] filas = {len(dataset)}")
|
||||
print(
|
||||
f"[CALIB] receta (env) = NUM_CALIBRATION_SAMPLES={NUM_CALIBRATION_SAMPLES} "
|
||||
f"NUM_ULTRACHAT_SAMPLES={NUM_ULTRACHAT_SAMPLES} MAX_SEQUENCE_LENGTH={MAX_SEQUENCE_LENGTH}"
|
||||
)
|
||||
print(f"[CALIB] TRAIN_DATA_PATH = {TRAIN_DATA_PATH} ({num_train} muestras de aca)")
|
||||
print(f"[CALIB] muestras de {ULTRACHAT_DATASET} = {NUM_ULTRACHAT_SAMPLES}")
|
||||
if from_cache:
|
||||
buckets_cache = (provenance or {}).get("bucket_counts")
|
||||
if buckets_cache:
|
||||
print(f"[CALIB] buckets (del provenance del cache) = {buckets_cache}")
|
||||
else:
|
||||
print(
|
||||
"[CALIB] buckets = no disponibles -- la muestra vino del cache ya "
|
||||
"tokenizado (que no conserva meta.bucket) y su provenance.json no los registro"
|
||||
)
|
||||
if provenance:
|
||||
print(f"[CALIB] cache generado el = {provenance.get('generated_at', '?')}")
|
||||
elif num_train == 0:
|
||||
print(
|
||||
"[CALIB] buckets = no aplica -- esta receta no usa TRAIN_DATA_PATH "
|
||||
"(NUM_ULTRACHAT_SAMPLES == NUM_CALIBRATION_SAMPLES): la muestra es 100% ultrachat"
|
||||
)
|
||||
else:
|
||||
print(f"[CALIB] buckets de {TRAIN_DATA_PATH.name} = {dict(LAST_TRAIN_BUCKET_COUNTS)}")
|
||||
if len(dataset) != NUM_CALIBRATION_SAMPLES:
|
||||
print(
|
||||
f"[CALIB] [WARN] la muestra tiene {len(dataset)} filas pero NUM_CALIBRATION_SAMPLES="
|
||||
f"{NUM_CALIBRATION_SAMPLES} -- NO fue generada con esta receta"
|
||||
)
|
||||
print("[CALIB] ====================================")
|
||||
|
||||
|
||||
class CalibrationDataCollator:
|
||||
"""Padding simple a la derecha -- sin labels, oneshot solo necesita forward pass."""
|
||||
|
||||
@@ -426,6 +664,10 @@ def prepare_calibration():
|
||||
CALIBRATION_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
calibration_dataset.save_to_disk(str(CALIBRATION_CACHE_PATH))
|
||||
print(f"[INFO] muestra de calibracion guardada en {CALIBRATION_CACHE_PATH} ({len(calibration_dataset)} ejemplos)")
|
||||
# Sidecar de procedencia: sin esto el cache es indistinguible del de cualquier
|
||||
# otra fase (mismo formato, y hasta el mismo conteo de filas).
|
||||
write_calibration_provenance(calibration_dataset)
|
||||
report_calibration_source(calibration_dataset, "GENERADA en este proceso y guardada en el cache")
|
||||
|
||||
|
||||
def main():
|
||||
@@ -450,6 +692,14 @@ def main():
|
||||
print(f"[INFO] cargando muestra de calibracion YA PREPARADA desde {CALIBRATION_CACHE_PATH}")
|
||||
calibration_dataset = Dataset.load_from_disk(str(CALIBRATION_CACHE_PATH))
|
||||
print(f"[INFO] {len(calibration_dataset)} ejemplos cargados desde el cache (sin tocar datasets/red)")
|
||||
# Aborta si el cache no corresponde a esta corrida (riesgo #1).
|
||||
provenance = verify_calibration_provenance(calibration_dataset)
|
||||
report_calibration_source(
|
||||
calibration_dataset,
|
||||
"CARGADA DEL CACHE EN DISCO (provenance.json verificado contra la receta actual)",
|
||||
from_cache=True,
|
||||
provenance=provenance,
|
||||
)
|
||||
else:
|
||||
print(f"[INFO] TRAIN_DATA_PATH={TRAIN_DATA_PATH}")
|
||||
print(f"[INFO] NUM_CALIBRATION_SAMPLES={NUM_CALIBRATION_SAMPLES} MAX_SEQUENCE_LENGTH={MAX_SEQUENCE_LENGTH}")
|
||||
@@ -459,6 +709,9 @@ def main():
|
||||
"calibracion en este mismo proceso (usar --prepare-calibration antes evita esto)"
|
||||
)
|
||||
calibration_dataset = load_calibration_dataset(tokenizer)
|
||||
report_calibration_source(
|
||||
calibration_dataset, "GENERADA en este mismo proceso (no habia cache en disco)"
|
||||
)
|
||||
|
||||
import gc
|
||||
|
||||
|
||||
Reference in New Issue
Block a user