Corregir lo que reportó ruff, que nunca se había ejecutado

ruff estaba configurado en pyproject.toml desde el primer commit y jamás se
había corrido. Tenía 15 hallazgos. Dos importan más allá del estilo:

- zip() sin strict= trunca en silencio al más corto. En las comparaciones de
  lotes eso significa que un test podía pasar sin haber comparado todo. Donde
  los largos deben coincidir ahora es strict=True; donde difieren a propósito
  (pares consecutivos) queda strict=False, que documenta la intención.
- Un import sin usar delataba algo peor: BraveBackend se había escrito sin una
  sola prueba. Se agregan ocho, contra una respuesta con la forma que devuelve
  la API, incluidas la limpieza de etiquetas, el caso de límite de tasa —que
  tiene que distinguirse de 'no respondió'— y que la credencial viaje en la
  cabecera y nunca en la URL. El respaldo tiene que funcionar justo cuando el
  primario ya falló; merecía la misma cobertura.

El resto es orden de imports, collections.abc y líneas largas.
This commit is contained in:
2026-07-28 07:25:51 -03:00
parent ba61125a3b
commit 4048936067
11 changed files with 185 additions and 64 deletions
+17 -14
View File
@@ -30,7 +30,10 @@ from dataclasses import dataclass
from typing import Protocol
# Un navegador real: el endpoint lite rechaza clientes sin User-Agent.
_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36"
_USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/125 Safari/537.36"
)
_RE_LINK = re.compile(
r"<a[^>]*?href=[\"'](?P<url>[^\"']+)[\"'][^>]*?class=['\"]result-link['\"][^>]*?>"
@@ -168,13 +171,17 @@ class BraveBackend:
self.language = language
def search(self, query: str, max_results: int) -> list[SearchResult]:
url = self.ENDPOINT + "?" + urllib.parse.urlencode(
{
"q": query,
"count": max_results,
"country": self.country,
"search_lang": self.language,
}
url = (
self.ENDPOINT
+ "?"
+ urllib.parse.urlencode(
{
"q": query,
"count": max_results,
"country": self.country,
"search_lang": self.language,
}
)
)
request = urllib.request.Request(
url,
@@ -283,9 +290,7 @@ class CadenaDeBackends:
self.ultimo_backend = nombre
return resultados
raise SearchError(
"ningún backend de búsqueda respondió — " + " | ".join(fallos)
)
raise SearchError("ningún backend de búsqueda respondió — " + " | ".join(fallos))
class WebSearch:
@@ -357,9 +362,7 @@ def _construir_uno(nombre: str, config) -> SearchBackend:
)
if nombre == "searxng":
if not config.searxng_url:
raise SearchError(
"backend searxng sin searxng_url: definí ENLACE_SEARXNG_URL en .env"
)
raise SearchError("backend searxng sin searxng_url: definí ENLACE_SEARXNG_URL en .env")
return SearxNGBackend(
base_url=config.searxng_url, timeout=config.timeout, language=config.language
)
-1
View File
@@ -16,7 +16,6 @@ from typing import Any
from omegaconf import DictConfig, OmegaConf
from enlace.config.env import load_dotenv
from enlace.config.schema import AgentConfig, Config, DistillConfig
# Familias de capas que puede declarar un YAML raíz, en el orden en que se
+6 -11
View File
@@ -33,9 +33,10 @@ from __future__ import annotations
import hashlib
import json
import time
from collections.abc import Iterable, Iterator
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Iterable, Iterator, Protocol
from typing import Any, Protocol
from enlace.config.env import load_dotenv
from enlace.config.schema import DistillConfig
@@ -200,8 +201,7 @@ def _normalizar_resultado(item: Any) -> dict[str, Any]:
"input_tokens": uso.input_tokens,
"output_tokens": uso.output_tokens,
"cache_read_input_tokens": getattr(uso, "cache_read_input_tokens", 0) or 0,
"cache_creation_input_tokens": getattr(uso, "cache_creation_input_tokens", 0)
or 0,
"cache_creation_input_tokens": getattr(uso, "cache_creation_input_tokens", 0) or 0,
}
elif tipo == "errored":
salida["error_type"] = item.result.error.type
@@ -268,8 +268,7 @@ class Trabajo:
def leer_peticiones(self) -> list[Peticion]:
if not self.peticiones_path.is_file():
raise DistillError(
f"el trabajo '{self.nombre}' no tiene manifiesto: "
f"falta {self.peticiones_path}"
f"el trabajo '{self.nombre}' no tiene manifiesto: falta {self.peticiones_path}"
)
peticiones = []
with self.peticiones_path.open(encoding="utf-8") as fh:
@@ -340,9 +339,7 @@ def submit(
else:
pendientes = unicas
ya_enviados = {
cid for lote in trabajo.leer_lotes() for cid in lote["custom_ids"]
}
ya_enviados = {cid for lote in trabajo.leer_lotes() for cid in lote["custom_ids"]}
pendientes = [p for p in pendientes if p.custom_id not in ya_enviados]
if not pendientes:
@@ -376,9 +373,7 @@ def submit(
return [lote["batch_id"] for lote in lotes]
def status(
trabajo: Trabajo, cliente: ClienteLotes
) -> list[dict[str, Any]]:
def status(trabajo: Trabajo, cliente: ClienteLotes) -> list[dict[str, Any]]:
"""Estado de cada lote del trabajo."""
estados = []
for lote in trabajo.leer_lotes():
+2 -3
View File
@@ -8,8 +8,8 @@ hardware decide; el modelo no sabe en qué placa corre.
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Iterator
import torch
from torch.nn.attention import SDPBackend, sdpa_kernel
@@ -40,8 +40,7 @@ def attention_backend(name: str) -> Iterator[None]:
backends = _BACKENDS[name]
except KeyError:
raise ValueError(
f"backend de atención desconocido: {name!r}. "
f"Válidos: {sorted(_BACKENDS)}."
f"backend de atención desconocido: {name!r}. Válidos: {sorted(_BACKENDS)}."
) from None
with sdpa_kernel(backends):
+41 -16
View File
@@ -32,7 +32,12 @@ import torch
from enlace.config.load import ConfigError, load_config
from enlace.config.schema import Config
from enlace.model.transformer import Transformer
from enlace.train.backends import autocast_context, build_grad_scaler, describe_device, setup_device
from enlace.train.backends import (
autocast_context,
build_grad_scaler,
describe_device,
setup_device,
)
from enlace.train.train import build_optimizer, flops_per_token
@@ -52,7 +57,10 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
resultados: list[Resultado] = []
print(f"[enlace] dispositivo: {describe_device(cfg.hardware)}")
print(f"[enlace] perfil {cfg.hardware.name} | modelo {cfg.model.name} | dtype {cfg.hardware.dtype}")
print(
f"[enlace] perfil {cfg.hardware.name} | modelo {cfg.model.name} "
f"| dtype {cfg.hardware.dtype}"
)
model = Transformer(cfg.model, cfg.hardware.attention_backend).to(device)
optimizer = build_optimizer(model, cfg)
@@ -76,7 +84,8 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
def micro_lote():
datos = torch.randint(
0, cfg.model.vocab_size,
0,
cfg.model.vocab_size,
(cfg.hardware.micro_batch_size, cfg.model.seq_len + 1),
generator=generador,
)
@@ -131,16 +140,22 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
uso < 0.92,
"VRAM",
f"pico reservado {pico_reservado:.2f} GB de {total:.1f} GB ({uso:.0%}). "
+ ("Entra con margen." if uso < 0.85 else
"Ajustado: un pico de fragmentación puede tirar la corrida." if uso < 0.92 else
"NO ENTRA con seguridad — bajá micro_batch_size o seq_len."),
+ (
"Entra con margen."
if uso < 0.85
else "Ajustado: un pico de fragmentación puede tirar la corrida."
if uso < 0.92
else "NO ENTRA con seguridad — bajá micro_batch_size o seq_len."
),
)
)
# --- rendimiento ---
medio = sum(tiempos) / len(tiempos)
tok_s = tokens_por_paso / medio
mfu = (fpt * tokens_por_paso / medio) / (cfg.hardware.peak_tflops * 1e12) if cfg.hardware.peak_tflops else None
mfu = None
if cfg.hardware.peak_tflops:
mfu = (fpt * tokens_por_paso / medio) / (cfg.hardware.peak_tflops * 1e12)
horas = cfg.train.max_steps * medio / 3600
tokens_totales = cfg.train.max_steps * tokens_por_paso
@@ -148,7 +163,7 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
Resultado(
True,
"Rendimiento",
f"{tok_s:,.0f} tok/s | {medio*1000:.0f} ms/paso"
f"{tok_s:,.0f} tok/s | {medio * 1000:.0f} ms/paso"
+ (f" | MFU {mfu:.1%}" if mfu else ""),
)
)
@@ -157,7 +172,7 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
True,
"Proyección",
f"{cfg.train.max_steps:,} pasos x {tokens_por_paso:,} tok = "
f"{tokens_totales/1e9:.2f}B tokens en ~{horas:.1f} h ({horas/24:.1f} días)",
f"{tokens_totales / 1e9:.2f}B tokens en ~{horas:.1f} h ({horas / 24:.1f} días)",
)
)
if mfu is not None:
@@ -165,14 +180,23 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
Resultado(
mfu > 0.15,
"MFU",
f"{mfu:.1%}{'razonable para esta placa' if mfu > 0.15 else 'bajo: revisá backend de atención, compile o tamaño de lote'}",
f"{mfu:.1%}"
+ (
"razonable para esta placa"
if mfu > 0.15
else "bajo: revisá backend de atención, compile o tamaño de lote"
),
)
)
# --- estabilidad ---
finitas = all(p == p and abs(p) != float("inf") for p in perdidas)
resultados.append(
Resultado(finitas, "Loss finita", f"{len(perdidas)} pasos medidos, todas finitas" if finitas else "apareció NaN o inf")
Resultado(
finitas,
"Loss finita",
f"{len(perdidas)} pasos medidos, todas finitas" if finitas else "apareció NaN o inf",
)
)
if cfg.hardware.use_grad_scaler:
@@ -182,15 +206,16 @@ def verificar(cfg: Config, pasos: int = 12, warmup: int = 3) -> list[Resultado]:
tasa <= 0.5,
"Estabilidad fp16",
f"{salteados}/{pasos} pasos salteados por el GradScaler"
+ (" (normal al calibrar la escala)" if tasa <= 0.5
else " — tasa alta: la corrida perdería actualizaciones"),
+ (
" (normal al calibrar la escala)"
if tasa <= 0.5
else " — tasa alta: la corrida perdería actualizaciones"
),
)
)
# --- parámetros ---
resultados.append(
Resultado(True, "Modelo", f"{n_params:,} parámetros ({n_params/1e6:.1f}M)")
)
resultados.append(Resultado(True, "Modelo", f"{n_params:,} parámetros ({n_params / 1e6:.1f}M)"))
return resultados