Ingeniería
Un valor por omisión que está invertido entre dos ficheros
Cuando la configuración de un modelo no menciona tie_word_embeddings, phi3 supone false y llama supone true. Copia cualquiera de los dos y obtienes un modelo que acierta en una familia y se equivoca en silencio en la otra.
Actualizado 2026-09-12Medido 2026-09-06
Aquí hay un defecto imposible de encontrar probando un solo modelo, y trivial de encontrar probando dos.
En mlx-lm, cuando el config.json de un modelo no menciona tie_word_embeddings, el valor por
omisión cambia según el fichero de arquitectura. phi3.py supone false. llama.py supone
true. Los dos son correctos para los modelos para los que se escribieron, y ninguno de los dos
es una regla general.
Así que, si estás portando una arquitectura y copias el valor por omisión del fichero que hayas leído primero, acertarás en esa familia y te equivocarás en la otra. Y «equivocarse» aquí no significa una excepción.
Qué controla realmente esa bandera
Un modelo de lenguaje tiene una tabla de incrustaciones que convierte, en la entrada, identificadores de token en vectores, y una proyección que convierte, en la salida, vectores otra vez en puntuaciones sobre el vocabulario. Esas dos tienen la misma forma, transpuesta.
Atadas significa que son los mismos pesos: la proyección de salida es la transpuesta de la
incrustación de entrada, y el fichero contiene una matriz. Desatadas significa que están
separadas, aprendidas de forma independiente, y el fichero contiene dos, la segunda llamada
normalmente lm_head.weight.
Atarlas ahorra muchísima memoria en un modelo pequeño, donde la matriz del vocabulario puede ser una parte sustancial de los parámetros. Desatarlas le da al modelo más capacidad. Distintas familias eligieron distinto, y por eso existe la bandera.
Por qué equivocarse resulta silencioso
Si supones atadas cuando el modelo las tiene desatadas, usas la incrustación de entrada como proyección de salida. La matriz tiene la forma correcta. La multiplicación sale bien. Obtienes logits.
Son logits calculados con la matriz equivocada, así que la distribución de salida del modelo no es la que aprendió a producir; pero tampoco es ruido, porque en un modelo entrenado con las dos matrices separadas ambas están emparentadas. El texto sigue siendo fluido. Solo empeora de una manera que se lee como «esta cuantización pierde un poco» o «este modelo no es gran cosa».
La dirección contraria es más ruidosa: suponer desatadas cuando los pesos están atados significa
buscar lm_head.weight y no encontrarlo, lo que al menos falla.
El arreglo consiste en dejar de adivinar
La respuesta fiable no es un valor por omisión mejor. Es mirar los pesos.
Si lm_head.weight está presente en los safetensors, las incrustaciones están desatadas, porque
un modelo desatado tiene que distribuir esa matriz y uno atado no tiene motivo para hacerlo. Si
falta, están atadas.
Esa comprobación no es una heurística. Es el hecho mismo: la presencia del tensor es lo que desatado significa a nivel de fichero. Un campo de configuración es una afirmación sobre el fichero, y donde ambos discrepan, gana el fichero.
Así que el orden es: leer el campo de configuración si existe y, si no existe, buscar el tensor. Nunca recurrir a una constante.
Comprobar un modelo en treinta segundos
Como la respuesta está en los pesos, esta es la receta práctica, que no exige descargar nada.
Abre el modelo en Hugging Face y mira la lista de ficheros. Si ves lm_head.weight en un
índice de safetensors, o un fichero cuyo nombre lo sugiera, las incrustaciones están desatadas. En
un modelo fragmentado, el fichero de índice —model.safetensors.index.json— tiene un weight_map
que enumera cada nombre de tensor, y es un fichero JSON corriente que puedes leer en el navegador.
Luego abre config.json y busca tie_word_embeddings. Tres desenlaces:
- Dice
truey no haylm_head.weight: coherente, atadas, nada que pensar. - Dice
falseylm_head.weightexiste: coherente, desatadas, igualmente. - No dice nada: este es el caso frecuente, y aquel en el que las dos bibliotecas discrepan. Deciden los pesos.
La cuarta combinación, en la que la configuración y los pesos se contradicen, es rara y conviene tratarla como un repositorio roto y no como un acertijo que resolver.
El patrón general
Esto es un caso de algo que merece reconocerse, porque se repite en todos los motores de modelos.
Las implementaciones de referencia se escriben por familia, y sus valores por omisión son
conocimiento local. Un valor en llama.py no es una afirmación sobre los modelos de lenguaje;
es una afirmación sobre llama. Leer un fichero y generalizar a partir de él es la forma de heredar
una suposición que nunca estuvo pensada para viajar.
La misma forma aparece en otro sitio del mismo código. Los sesgos de atención, por ejemplo: qwen2
los tiene en las proyecciones de consulta, clave y valor, y no los declara en ninguna parte,
porque en mlx-lm viven en la arquitectura y no en la configuración. Llama los hace opcionales a
través de attention_bias. Una portación que solo lea la configuración se equivoca con qwen2; una
que añada siempre los sesgos se equivoca con llama. Mirar si el tensor existe responde a las dos.
Que es la misma regla de antes, y se generaliza:
Cuando la configuración y los pesos discrepan sobre qué es un modelo, los pesos son el modelo.
Cómo cazarlo antes de publicar
El único motivo por el que esto se encontró en vez de publicarse es que aquí la corrección se comprueba contra una referencia y no leyendo la salida.
mlx-lm genera oráculos: para un modelo y un prompt dados, la secuencia exacta de tokens y los
logits en crudo. Una portación tiene que reproducirlos. Una proyección de salida equivocada cambia
los logits de inmediato y de forma visible —en números, no en sensaciones— y la prueba falla en el
primer token.
Si estás portando arquitecturas y no tienes eso, conviene construirlo antes de la segunda familia en vez de después de la décima. La alternativa es leer texto generado y formarse una impresión, y una impresión no puede distinguir «matriz equivocada» de «modelo pequeño».
Una advertencia sobre comparar logits, ya que es el trabajo de la misma tarde: la tolerancia no puede bajar de un paso del tipo de los logits. Qwen3 produce logits en bfloat16, donde a una magnitud de 16 un paso vale 0,125; exigir 0,05 es, por tanto, exigir igualdad bit a bit llamándolo tolerancia.
Qué cuesta cuando está bien
Merece un párrafo, porque la bandera no sale gratis en ninguna de las dos direcciones y el intercambio es real.
En un modelo pequeño la matriz del vocabulario es una gran parte de todo. Un modelo con un vocabulario de 128.000 tokens y un tamaño oculto de 2.048 lleva unos 262 millones de parámetros solo en esa matriz, lo que en un modelo de mil millones de parámetros viene a ser una cuarta parte. Atarlas reduce ese coste a la mitad, y en un modelo así de pequeño la memoria ahorrada es la diferencia entre caber con holgura y no caber.
En un modelo grande esa misma matriz es un error de redondeo, y la capacidad extra que da desatarlas vale más que la memoria. Por eso la elección va correlacionada con el tamaño, y por eso un valor por omisión que era correcto para una familia resulta equivocado para la otra: las dos bibliotecas no estaban siendo descuidadas. Cada una describía sus propios modelos.
La versión corta
tie_word_embeddingsvale por omisiónfalseen phi3 ytrueen llama, dentro de la misma biblioteca.- Equivocarse produce texto fluido y peor, no un error.
- No elijas un valor por omisión: comprueba si
lm_head.weightestá en los pesos. - La misma regla vale para los sesgos de atención y para cualquier otra cosa que la configuración pueda omitir.
Una última cosa, ya que es la pregunta que esto siempre levanta: no, no puedes saberlo por el nombre del modelo, por su número de parámetros ni por su familia. Dos modelos del mismo editor, publicados con un mes de diferencia, pueden ser distintos. La lista de ficheros es el único sitio donde vive la respuesta.
Comprobado contra mlx-lm 0.31.3 el 6 de septiembre de 2026.