4048936067
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.
144 lines
4.4 KiB
Python
144 lines
4.4 KiB
Python
"""Cargadores de datos: determinismo, reanudación y recorrido de shards."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from enlace.data.loaders import ByteStream, ShardStream
|
|
|
|
|
|
def _byte_stream(texto, seed=1337):
|
|
return ByteStream(
|
|
text_path=texto,
|
|
batch_size=2,
|
|
seq_len=16,
|
|
val_fraction=0.1,
|
|
seed=seed,
|
|
device=torch.device("cpu"),
|
|
)
|
|
|
|
|
|
def test_los_targets_son_la_entrada_desplazada_un_token(texto_es):
|
|
stream = _byte_stream(texto_es)
|
|
x, y = stream.next_batch("train")
|
|
assert x.shape == y.shape == (2, 16)
|
|
assert torch.equal(x[:, 1:], y[:, :-1])
|
|
|
|
|
|
def test_la_misma_semilla_da_los_mismos_lotes(texto_es):
|
|
a = _byte_stream(texto_es)
|
|
b = _byte_stream(texto_es)
|
|
for _ in range(3):
|
|
xa, _ = a.next_batch("train")
|
|
xb, _ = b.next_batch("train")
|
|
assert torch.equal(xa, xb)
|
|
|
|
|
|
def test_restaurar_el_estado_continua_la_misma_secuencia(texto_es):
|
|
a = _byte_stream(texto_es)
|
|
for _ in range(5):
|
|
a.next_batch("train")
|
|
estado = a.state_dict()
|
|
esperado = [a.next_batch("train")[0] for _ in range(3)]
|
|
|
|
b = _byte_stream(texto_es)
|
|
b.load_state_dict(estado)
|
|
obtenido = [b.next_batch("train")[0] for _ in range(3)]
|
|
|
|
for e, o in zip(esperado, obtenido, strict=True):
|
|
assert torch.equal(e, o)
|
|
|
|
|
|
def test_evaluar_no_altera_la_secuencia_de_entrenamiento(texto_es):
|
|
"""Generadores separados por split: cambiar eval_every no debe mover los
|
|
lotes de entrenamiento, o dos corridas dejarían de ser comparables."""
|
|
a = _byte_stream(texto_es)
|
|
esperado = [a.next_batch("train")[0] for _ in range(3)]
|
|
|
|
b = _byte_stream(texto_es)
|
|
lotes = []
|
|
for _ in range(3):
|
|
b.next_batch("val")
|
|
lotes.append(b.next_batch("train")[0])
|
|
|
|
for e, o in zip(esperado, lotes, strict=True):
|
|
assert torch.equal(e, o)
|
|
|
|
|
|
def test_el_vocabulario_de_bytes_es_siempre_256(texto_es):
|
|
assert _byte_stream(texto_es).vocab_size == 256
|
|
|
|
|
|
def test_decodifica_utf8_con_acentos():
|
|
assert ByteStream.decode(list("El niño está acá.".encode())) == "El niño está acá."
|
|
|
|
|
|
def test_texto_inexistente_da_un_error_util(tmp_path):
|
|
with pytest.raises(FileNotFoundError, match="prepare_smoke_data"):
|
|
_byte_stream(tmp_path / "no-existe.txt")
|
|
|
|
|
|
def _shard_stream(directory, batch_size=2, seq_len=8):
|
|
return ShardStream(
|
|
shards_dir=directory,
|
|
batch_size=batch_size,
|
|
seq_len=seq_len,
|
|
val_fraction=0.1,
|
|
device=torch.device("cpu"),
|
|
)
|
|
|
|
|
|
def test_los_shards_se_recorren_en_orden(shards_dir):
|
|
stream = _shard_stream(shards_dir)
|
|
x, y = stream.next_batch("train")
|
|
# El fixture escribe tokens consecutivos: 0, 1, 2, ...
|
|
assert x[0].tolist() == list(range(8))
|
|
assert y[0].tolist() == list(range(1, 9))
|
|
assert x[1].tolist() == list(range(8, 16))
|
|
|
|
x2, _ = stream.next_batch("train")
|
|
assert x2[0].tolist() == list(range(16, 24))
|
|
|
|
|
|
def test_la_lectura_cruza_el_limite_entre_shards(shards_dir):
|
|
"""Un lote que empieza cerca del final de un shard tiene que continuar en el
|
|
siguiente sin saltarse ni repetir tokens."""
|
|
stream = _shard_stream(shards_dir, batch_size=1, seq_len=8)
|
|
stream.load_state_dict({"train": 4090})
|
|
x, _ = stream.next_batch("train")
|
|
assert x[0].tolist() == list(range(4090, 4098)) # cruza de shard 0 a shard 1
|
|
|
|
|
|
def test_restaurar_la_posicion_reanuda_donde_iba(shards_dir):
|
|
a = _shard_stream(shards_dir)
|
|
for _ in range(4):
|
|
a.next_batch("train")
|
|
estado = a.state_dict()
|
|
esperado, _ = a.next_batch("train")
|
|
|
|
b = _shard_stream(shards_dir)
|
|
b.load_state_dict(estado)
|
|
obtenido, _ = b.next_batch("train")
|
|
assert torch.equal(esperado, obtenido)
|
|
|
|
|
|
def test_al_terminar_la_epoca_vuelve_al_principio(shards_dir):
|
|
stream = _shard_stream(shards_dir, batch_size=1, seq_len=8)
|
|
primero, _ = stream.next_batch("train")
|
|
# El split de train son 7372 tokens; posicionarse casi al final.
|
|
stream.load_state_dict({"train": 7370})
|
|
stream.next_batch("train")
|
|
reiniciado, _ = stream.next_batch("train")
|
|
assert reiniciado[0, 0].item() == 8 # segundo lote de la época nueva
|
|
|
|
|
|
def test_shards_faltantes_dan_un_error_util(tmp_path):
|
|
with pytest.raises(FileNotFoundError, match="prepare_data"):
|
|
_shard_stream(tmp_path)
|
|
|
|
|
|
def test_corpus_demasiado_chico_para_el_lote(shards_dir):
|
|
with pytest.raises(ValueError, match="menos que los"):
|
|
_shard_stream(shards_dir, batch_size=64, seq_len=1024)
|