Compare commits
10
Commits
2742c55fdd
...
966811d54f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966811d54f
|
||
|
|
2d2c45f0fe
|
||
|
|
bc1638f2da
|
||
|
|
77e6804a4f
|
||
|
|
6419646133
|
||
|
|
be90e51214
|
||
|
|
b246f8d97a
|
||
|
|
78d9b0d90d
|
||
|
|
6f1db06f06
|
||
|
|
d1e9892911
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"eval": [
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Termine de arreglar el bug del login, aplica aleleba-pr para subir esto.",
|
||||
"passed": false,
|
||||
"hits": []
|
||||
},
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Aplica aleleba-pr sobre estos cambios, pero NO lo mergees vos.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"no",
|
||||
"merge"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "docmost-context",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Estamos arrancando una conversacion nueva sobre el proyecto qwen3-6-lora, carga el contexto de Docmost.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"docmost"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "agent-orchestrator",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Quiero dejar esto trabajando en background, lanza un agente para migrar la base de datos.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"agente"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "web-ui-test",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Prueba la interfaz de login de la web app y sacame capturas del flujo completo.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"captura"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que opinas de usar pull requests chicos en vez de uno grande, en general?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "agent-orchestrator",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que significa correr un proceso en background en Linux?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "docmost-context",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que es Docmost, para que sirve como herramienta?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "web-ui-test",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que es Playwright y en que se diferencia de Selenium?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "spark-ssh",
|
||||
"kind": "no_activacion_held_out",
|
||||
"prompt": "¿Como se hace una conexion SSH normalmente en Linux?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
}
|
||||
],
|
||||
"baseline": null,
|
||||
"baseline_disponible": false
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"eval": [
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Termine de arreglar el bug del login, aplica aleleba-pr para subir esto.",
|
||||
"passed": false,
|
||||
"hits": []
|
||||
},
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Aplica aleleba-pr sobre estos cambios, pero NO lo mergees vos.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"no",
|
||||
"merge"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "docmost-context",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Estamos arrancando una conversacion nueva sobre el proyecto qwen3-6-lora, carga el contexto de Docmost.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"docmost"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "agent-orchestrator",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Quiero dejar esto trabajando en background, lanza un agente para migrar la base de datos.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"agente"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "web-ui-test",
|
||||
"kind": "adherencia",
|
||||
"prompt": "Prueba la interfaz de login de la web app y sacame capturas del flujo completo.",
|
||||
"passed": true,
|
||||
"hits": [
|
||||
"screenshot",
|
||||
"captura"
|
||||
]
|
||||
},
|
||||
{
|
||||
"skill": "aleleba-pr",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que opinas de usar pull requests chicos en vez de uno grande, en general?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "agent-orchestrator",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que significa correr un proceso en background en Linux?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "docmost-context",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que es Docmost, para que sirve como herramienta?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "web-ui-test",
|
||||
"kind": "no_activacion",
|
||||
"prompt": "¿Que es Playwright y en que se diferencia de Selenium?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
},
|
||||
{
|
||||
"skill": "spark-ssh",
|
||||
"kind": "no_activacion_held_out",
|
||||
"prompt": "¿Como se hace una conexion SSH normalmente en Linux?",
|
||||
"passed": true,
|
||||
"tool_calls": []
|
||||
}
|
||||
],
|
||||
"baseline": null,
|
||||
"baseline_disponible": false
|
||||
}
|
||||
@@ -80,3 +80,56 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 600s
|
||||
|
||||
# Fase 5 -- diagnostico de aislamiento: identico a vllm-eval-nvfp4 pero SIN
|
||||
# --speculative-config, para determinar si la regresion de calidad observada
|
||||
# en las puertas 2-3 (vs. Fase 4) viene del speculative decoding (MTP) o de
|
||||
# la cuantizacion NVFP4 en si. Puerto 8003 (distinto de 8000 produccion, 8001
|
||||
# Fase 4, 8002 Fase 5 con speculative real). Servicio temporal de diagnostico,
|
||||
# no clona produccion 1:1 a proposito (esa es la variable que se esta aislando).
|
||||
vllm-eval-nvfp4-nospec:
|
||||
image: vllm/vllm-openai:cu130-nightly-aarch64
|
||||
container_name: vllm-eval-nvfp4-nospec
|
||||
restart: "no"
|
||||
runtime: nvidia
|
||||
environment:
|
||||
NVIDIA_VISIBLE_DEVICES: all
|
||||
NVIDIA_DRIVER_CAPABILITIES: compute,utility
|
||||
ports:
|
||||
- "8003:8000"
|
||||
ipc: host
|
||||
ulimits:
|
||||
memlock: -1
|
||||
stack: 67108864
|
||||
volumes:
|
||||
- /home/aleleba/ft-models/Qwen3.6-35B-A3B-mcp-NVFP4:/models/Qwen3.6-35B-A3B-mcp-NVFP4:ro
|
||||
command: >
|
||||
--model /models/Qwen3.6-35B-A3B-mcp-NVFP4
|
||||
--served-model-name qwen3.6-35b-a3b-mcp-nvfp4-nospec
|
||||
--host 0.0.0.0
|
||||
--port 8000
|
||||
--tensor-parallel-size 1
|
||||
--trust-remote-code
|
||||
--quantization compressed-tensors
|
||||
--moe-backend flashinfer_cutlass
|
||||
--kv-cache-dtype fp8_e4m3
|
||||
--gpu-memory-utilization 0.45
|
||||
--max-model-len 524288
|
||||
--max-num-seqs 8
|
||||
--max-num-batched-tokens 32768
|
||||
--enable-chunked-prefill
|
||||
--enable-prefix-caching
|
||||
--reasoning-parser qwen3
|
||||
--tool-call-parser qwen3_coder
|
||||
--enable-auto-tool-choice
|
||||
--default-chat-template-kwargs '{"preserve_thinking":true}'
|
||||
--limit-mm-per-prompt '{"image":4,"video":0,"audio":0}'
|
||||
--generation-config vllm
|
||||
--override-generation-config '{"temperature":0.6,"top_p":0.80,"top_k":20,"presence_penalty":0.0,"repetition_penalty":1.0}'
|
||||
--hf-overrides '{"text_config":{"rope_scaling":{"rope_type":"yarn","factor":2.0,"original_max_position_embeddings":262144}}}'
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 600s
|
||||
|
||||
@@ -13,6 +13,14 @@ 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
|
||||
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
|
||||
tokenizar -- experimento para probar si la regresion de calidad viene de poca
|
||||
DIVERSIDAD tematica en la calibracion (solo conversaciones angostas de los 5
|
||||
MCPs/skills) en vez de poca CANTIDAD de muestras.
|
||||
|
||||
Algoritmo:
|
||||
1. Cargar Qwen3_5MoeForConditionalGeneration.from_pretrained(MODEL_PATH,
|
||||
dtype="auto") + AutoProcessor.from_pretrained(MODEL_PATH).
|
||||
@@ -42,6 +50,18 @@ Algoritmo:
|
||||
|
||||
Soporta --verify-only (o env var VERIFY_ONLY=1) para re-correr solo las
|
||||
verificaciones sobre un OUTPUT_PATH ya generado, sin repetir la calibracion.
|
||||
|
||||
Soporta --prepare-calibration (o env var PREPARE_CALIBRATION=1) para construir y
|
||||
guardar en disco (CALIBRATION_CACHE_PATH) la muestra de calibracion ya tokenizada,
|
||||
SIN cargar el modelo. Corre esto en un proceso APARTE, antes de la cuantizacion
|
||||
normal (que detecta el cache y lo carga en vez de reconstruirlo). Motivo: cargar
|
||||
`datasets`/streamear ultrachat_200k y cargar el modelo de 67GB en el MISMO proceso
|
||||
empuja la presion de memoria del pool unificado del GB10 justo al borde --
|
||||
confirmado empiricamente que hasta una corrida SIN ultrachat (NUM_CALIBRATION_SAMPLES
|
||||
=1024, solo TRAIN_DATA_PATH) crasheo con el mismo CUDA OOM que las corridas
|
||||
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.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -77,8 +97,31 @@ MODEL_PATH = Path(os.environ.get("MODEL_PATH", "/workspace/ft-models/Qwen3.6-35B
|
||||
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"))
|
||||
# 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"))
|
||||
ULTRACHAT_DATASET = "HuggingFaceH4/ultrachat_200k"
|
||||
ULTRACHAT_SPLIT = "train_sft"
|
||||
MAX_SEQUENCE_LENGTH = int(os.environ.get("MAX_SEQUENCE_LENGTH", "8192"))
|
||||
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
|
||||
# 67GB completo) en el MISMO proceso empuja la presion de memoria del pool
|
||||
# unificado GB10 justo al borde -- confirmado empiricamente: incluso una corrida
|
||||
# de solo TRAIN_DATA_PATH con NUM_CALIBRATION_SAMPLES=1024 (sin ultrachat) crasheo
|
||||
# con el mismo OOM que las corridas mezcladas, en el mismo punto exacto del setup
|
||||
# de oneshot() (disable_lm_head). CALIBRATION_CACHE_PATH separa ambos pasos en dos
|
||||
# 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.
|
||||
CALIBRATION_CACHE_PATH = Path(
|
||||
os.environ.get("CALIBRATION_CACHE_PATH", "/workspace/ft-models/nvfp4_calibration_cache")
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -95,7 +138,7 @@ QUANTIZATION_IGNORE = [
|
||||
]
|
||||
|
||||
|
||||
def load_calibration_dataset(tokenizer):
|
||||
def load_train_examples(n):
|
||||
print(f"[INFO] cargando ejemplos de calibracion desde {TRAIN_DATA_PATH}")
|
||||
examples = []
|
||||
with open(TRAIN_DATA_PATH, encoding="utf-8") as f:
|
||||
@@ -107,21 +150,63 @@ def load_calibration_dataset(tokenizer):
|
||||
|
||||
rng = random.Random(CALIBRATION_SEED)
|
||||
rng.shuffle(examples)
|
||||
sampled = examples[:NUM_CALIBRATION_SAMPLES]
|
||||
if len(sampled) < NUM_CALIBRATION_SAMPLES:
|
||||
sampled = examples[:n]
|
||||
if len(sampled) < n:
|
||||
raise AssertionError(
|
||||
f"se pidieron {NUM_CALIBRATION_SAMPLES} muestras de calibracion pero "
|
||||
f"{TRAIN_DATA_PATH} solo tiene {len(examples)} ejemplos"
|
||||
f"se pidieron {n} muestras de {TRAIN_DATA_PATH} pero solo tiene {len(examples)} ejemplos"
|
||||
)
|
||||
|
||||
from collections import Counter
|
||||
|
||||
bucket_counts = Counter(ex.get("meta", {}).get("bucket", "?") for ex in sampled)
|
||||
print(f"[INFO] distribucion de buckets en la muestra de calibracion: {dict(bucket_counts)}")
|
||||
print(f"[INFO] distribucion de buckets (train.jsonl): {dict(bucket_counts)}")
|
||||
return sampled
|
||||
|
||||
|
||||
def load_ultrachat_examples(n):
|
||||
from datasets import load_dataset
|
||||
|
||||
# streaming=True: el split train_sft completo tiene ~208k ejemplos (~2.9GB
|
||||
# materializados como Arrow por load_dataset sin streaming, las 4 splits del
|
||||
# repo se generan igual). En este hardware (GB10, memoria unificada CPU/GPU)
|
||||
# ese cache extra resulto ser suficiente para tirar un CUDA OOM reproducible
|
||||
# durante el setup de oneshot() (trace_subgraphs/disable_lm_head), incluso
|
||||
# truncando las secuencias a 2048 tokens -- el problema no era el largo de
|
||||
# secuencia sino la memoria consumida por materializar el dataset completo.
|
||||
# Con streaming solo se bajan los ~n ejemplos necesarios, sin cache local.
|
||||
print(f"[INFO] cargando {n} muestras de {ULTRACHAT_DATASET} (split={ULTRACHAT_SPLIT}, streaming)")
|
||||
ds = load_dataset(ULTRACHAT_DATASET, split=ULTRACHAT_SPLIT, streaming=True)
|
||||
ds = ds.shuffle(seed=CALIBRATION_SEED, buffer_size=10_000)
|
||||
examples = [{"messages": row["messages"]} for row in ds.take(n)]
|
||||
print(f"[INFO] {len(examples)} muestras de {ULTRACHAT_DATASET} cargadas (streaming, sin materializar el dataset completo)")
|
||||
return examples
|
||||
|
||||
|
||||
def load_calibration_dataset(tokenizer):
|
||||
num_ultrachat = NUM_ULTRACHAT_SAMPLES
|
||||
num_train = NUM_CALIBRATION_SAMPLES - num_ultrachat
|
||||
if num_train < 0:
|
||||
raise AssertionError(
|
||||
f"NUM_ULTRACHAT_SAMPLES ({num_ultrachat}) no puede superar "
|
||||
f"NUM_CALIBRATION_SAMPLES ({NUM_CALIBRATION_SAMPLES})"
|
||||
)
|
||||
|
||||
examples = []
|
||||
if num_train > 0:
|
||||
examples.extend(load_train_examples(num_train))
|
||||
if num_ultrachat > 0:
|
||||
examples.extend(load_ultrachat_examples(num_ultrachat))
|
||||
|
||||
rng = random.Random(CALIBRATION_SEED)
|
||||
rng.shuffle(examples)
|
||||
print(
|
||||
f"[INFO] muestra de calibracion mezclada: {num_train} de {TRAIN_DATA_PATH.name} + "
|
||||
f"{num_ultrachat} de {ULTRACHAT_DATASET}, {len(examples)} total, orden mezclado (seed={CALIBRATION_SEED})"
|
||||
)
|
||||
|
||||
input_ids_list = []
|
||||
attention_mask_list = []
|
||||
for ex in sampled:
|
||||
for ex in examples:
|
||||
text = tokenizer.apply_chat_template(
|
||||
ex["messages"],
|
||||
tools=ex.get("tools"),
|
||||
@@ -314,26 +399,71 @@ def parse_args():
|
||||
default=os.environ.get("VERIFY_ONLY", "") not in ("", "0", "false", "False"),
|
||||
help="saltar calibracion/guardado, solo re-correr las verificaciones sobre OUTPUT_PATH ya existente",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prepare-calibration",
|
||||
action="store_true",
|
||||
default=os.environ.get("PREPARE_CALIBRATION", "") not in ("", "0", "false", "False"),
|
||||
help=(
|
||||
"solo construir y guardar en disco (CALIBRATION_CACHE_PATH) la muestra de "
|
||||
"calibracion ya tokenizada, SIN cargar el modelo -- correr en un proceso aparte "
|
||||
"antes de la cuantizacion, para que `datasets`/streaming/red nunca compartan "
|
||||
"proceso con el modelo de 67GB"
|
||||
),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def prepare_calibration():
|
||||
print(f"[INFO] TRAIN_DATA_PATH={TRAIN_DATA_PATH}")
|
||||
print(f"[INFO] NUM_CALIBRATION_SAMPLES={NUM_CALIBRATION_SAMPLES} MAX_SEQUENCE_LENGTH={MAX_SEQUENCE_LENGTH}")
|
||||
print(f"[INFO] NUM_ULTRACHAT_SAMPLES={NUM_ULTRACHAT_SAMPLES}")
|
||||
print(f"[INFO] cargando tokenizer desde {MODEL_PATH} (solo tokenizer, no el modelo)")
|
||||
processor = AutoProcessor.from_pretrained(str(MODEL_PATH), trust_remote_code=True)
|
||||
tokenizer = processor.tokenizer
|
||||
|
||||
calibration_dataset = load_calibration_dataset(tokenizer)
|
||||
|
||||
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)")
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
print(f"[INFO] MODEL_PATH={MODEL_PATH}")
|
||||
print(f"[INFO] OUTPUT_PATH={OUTPUT_PATH}")
|
||||
|
||||
if args.prepare_calibration:
|
||||
prepare_calibration()
|
||||
return
|
||||
|
||||
if args.verify_only:
|
||||
print("[INFO] --verify-only: saltando calibracion/guardado, solo verificando OUTPUT_PATH existente")
|
||||
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}")
|
||||
print(f"[INFO] CALIBRATION_CACHE_PATH={CALIBRATION_CACHE_PATH}")
|
||||
|
||||
print(f"[INFO] cargando processor desde {MODEL_PATH}")
|
||||
processor = AutoProcessor.from_pretrained(str(MODEL_PATH), trust_remote_code=True)
|
||||
tokenizer = processor.tokenizer
|
||||
|
||||
if CALIBRATION_CACHE_PATH.exists():
|
||||
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)")
|
||||
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}")
|
||||
print(f"[INFO] NUM_ULTRACHAT_SAMPLES={NUM_ULTRACHAT_SAMPLES}")
|
||||
print(
|
||||
f"[INFO] no hay cache en {CALIBRATION_CACHE_PATH} -- construyendo la muestra de "
|
||||
"calibracion en este mismo proceso (usar --prepare-calibration antes evita esto)"
|
||||
)
|
||||
calibration_dataset = load_calibration_dataset(tokenizer)
|
||||
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
print(f"[INFO] cargando modelo desde {MODEL_PATH} (dtype=auto)")
|
||||
t_load = time.time()
|
||||
model = Qwen3_5MoeForConditionalGeneration.from_pretrained(
|
||||
|
||||
@@ -35,6 +35,10 @@ HOLDOUT_PATH = REPO_ROOT / "data" / "holdout_prompts.jsonl"
|
||||
RESULTS_PATH = REPO_ROOT / "data" / os.environ.get("GATE2_RESULTS_FILENAME", "gate2_results.json")
|
||||
BASE_URL = os.environ.get("VLLM_EVAL_URL", "http://localhost:8001")
|
||||
MODEL_NAME = os.environ.get("VLLM_EVAL_MODEL", "qwen3.6-35b-a3b-mcp-bf16")
|
||||
# 1024 dejaba cortar la respuesta a mitad de razonamiento en modelos con
|
||||
# --reasoning-parser activo antes de emitir el tool_call -- ver hallazgo de
|
||||
# Fase 5 (misma causa que el fix de gate3, max_tokens 512->2048).
|
||||
GATE2_MAX_TOKENS = int(os.environ.get("GATE2_MAX_TOKENS", "2048"))
|
||||
|
||||
|
||||
def load_holdout():
|
||||
@@ -92,13 +96,13 @@ def validate_tool_call(tool_call, tools):
|
||||
return True, None
|
||||
|
||||
|
||||
def call_vllm(prompt, tools, timeout=120):
|
||||
def call_vllm(prompt, tools, timeout=240):
|
||||
payload = {
|
||||
"model": MODEL_NAME,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"tools": to_openai_tools(tools),
|
||||
"tool_choice": "auto",
|
||||
"max_tokens": 1024,
|
||||
"max_tokens": GATE2_MAX_TOKENS,
|
||||
"temperature": 0.0,
|
||||
}
|
||||
resp = requests.post(f"{BASE_URL}/v1/chat/completions", json=payload, timeout=timeout)
|
||||
@@ -133,11 +137,24 @@ def main():
|
||||
continue
|
||||
|
||||
message = response["choices"][0]["message"]
|
||||
# Se guarda siempre el texto completo (content + reasoning) para poder auditar
|
||||
# con criterio humano los casos que fallan o quedan sin tool_call -- antes no se
|
||||
# guardaba nada de esto, lo que hacia imposible diagnosticar truncamiento.
|
||||
content = message.get("content") or ""
|
||||
reasoning = message.get("reasoning") or ""
|
||||
tool_calls = message.get("tool_calls") or []
|
||||
if not tool_calls:
|
||||
stats[mcp]["no_tool_call"] += 1
|
||||
stats["__global__"]["no_tool_call"] += 1
|
||||
results.append({"mcp": mcp, "prompt": ex["prompt"], "tool_calls": None, "valid": None})
|
||||
results.append({
|
||||
"mcp": mcp,
|
||||
"prompt": ex["prompt"],
|
||||
"tool_calls": None,
|
||||
"valid": None,
|
||||
"content": content,
|
||||
"reasoning": reasoning,
|
||||
"finish_reason": response["choices"][0].get("finish_reason"),
|
||||
})
|
||||
continue
|
||||
|
||||
all_valid = True
|
||||
@@ -161,6 +178,9 @@ def main():
|
||||
"tool_calls": [tc["function"]["name"] for tc in tool_calls],
|
||||
"valid": all_valid,
|
||||
"errors": errors,
|
||||
"content": content,
|
||||
"reasoning": reasoning,
|
||||
"finish_reason": response["choices"][0].get("finish_reason"),
|
||||
})
|
||||
|
||||
if (i + 1) % 20 == 0:
|
||||
|
||||
@@ -33,6 +33,11 @@ EVAL_URL = os.environ.get("VLLM_EVAL_URL", "http://localhost:8001")
|
||||
EVAL_MODEL = os.environ.get("VLLM_EVAL_MODEL", "qwen3.6-35b-a3b-mcp-bf16")
|
||||
PROD_URL = os.environ.get("VLLM_PROD_URL", "http://localhost:8000")
|
||||
PROD_MODEL = os.environ.get("VLLM_PROD_MODEL", "qwen3.6-35b-a3b")
|
||||
# 512 dejaba cortar la respuesta a mitad de razonamiento en los prompts mas
|
||||
# abiertos (modelo de razonamiento con --reasoning-parser activo) antes de
|
||||
# emitir el contenido final -- un FAIL por presupuesto de tokens agotado, no
|
||||
# por adherencia real. Ver hallazgo de Fase 5.
|
||||
GATE3_MAX_TOKENS = int(os.environ.get("GATE3_MAX_TOKENS", "2048"))
|
||||
|
||||
# Checklists por skill: (skill, tipo, prompt, [substrings esperados en la respuesta, any-of])
|
||||
CHECKLISTS = [
|
||||
@@ -103,10 +108,10 @@ def call_model(base_url, model_name, prompt):
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 512,
|
||||
"max_tokens": GATE3_MAX_TOKENS,
|
||||
"temperature": 0.0,
|
||||
}
|
||||
resp = requests.post(f"{base_url}/v1/chat/completions", json=payload, timeout=120)
|
||||
resp = requests.post(f"{base_url}/v1/chat/completions", json=payload, timeout=240)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
message = data["choices"][0]["message"]
|
||||
@@ -147,9 +152,20 @@ def run_checklist(base_url, model_name, label):
|
||||
print(f" [ERROR] {skill}/{kind}: {e}")
|
||||
continue
|
||||
|
||||
# Se guarda siempre el texto completo (content + reasoning) para poder auditar
|
||||
# con criterio humano los casos que fallan -- antes solo se guardaba passed/hits,
|
||||
# lo que hacia imposible revisar despues que dijo realmente el modelo.
|
||||
if kind == "adherencia":
|
||||
passed, hits = check_adherencia(response, expected)
|
||||
rows.append({"skill": skill, "kind": kind, "prompt": prompt, "passed": passed, "hits": hits})
|
||||
rows.append({
|
||||
"skill": skill,
|
||||
"kind": kind,
|
||||
"prompt": prompt,
|
||||
"passed": passed,
|
||||
"hits": hits,
|
||||
"content": response["content"],
|
||||
"reasoning": response["reasoning"],
|
||||
})
|
||||
print(f" {'OK ' if passed else 'FAIL'} {skill:20s} adherencia hits={hits}")
|
||||
else:
|
||||
passed = check_no_activacion(response)
|
||||
@@ -159,6 +175,8 @@ def run_checklist(base_url, model_name, label):
|
||||
"prompt": prompt,
|
||||
"passed": passed,
|
||||
"tool_calls": [tc["function"]["name"] for tc in response["tool_calls"]],
|
||||
"content": response["content"],
|
||||
"reasoning": response["reasoning"],
|
||||
})
|
||||
print(f" {'OK ' if passed else 'FAIL'} {skill:20s} {kind:20s} tool_calls={len(response['tool_calls'])}")
|
||||
return rows
|
||||
|
||||
Reference in New Issue
Block a user