transformadores y esperamos que en su interior surjan circuitos útiles de reconocimiento de patrones. Pero ¿y si ya conociéramos el circuito? ¿Qué pasaría si, en lugar de aprender los pesos de los datos, los construyéramos analíticamente para que el modelo ejecutara un gráfico de cálculo directamente?
Esa es la idea detrás de mi proyecto de fin de semana de Pascua.
En lugar de tratar un transformador como un sistema que debe descubrir un algoritmo mediante optimización, lo trato como una máquina programable. Un cronograma especifica qué cantidades intermedias deben calcularse en cada paso. Se asignan dimensiones ocultas a variables como registros en una computadora diminuta. Los cabezales de atención están conectados para realizar búsquedas y enrutamiento. Las unidades de retroalimentación están conectadas para implementar cálculos controlados locales. Las actualizaciones residuales escriben el siguiente estado de la máquina en la secuencia.
El resultado es un transformador básico que ejecuta un programa determinista.
Desde este punto de vista, el flujo residual es la memoria de trabajo y cada capa se convierte en un paso de la máquina. Algunos valores se leen, otros se transforman, otros se pasan y otros se sobrescriben cuando sus ranuras se pueden reutilizar de forma segura. El transformador comienza a parecerse a una pequeña computadora compilada construida a partir de atención, proyecciones lineales y bloques de control.
Nada de esto requiere formación. Si ya tiene un gráfico de cálculo y un cronograma de cuándo debe existir cada variable intermedia, puede construir directamente las ponderaciones del modelo. Como resultado, el transformador se convierte en un motor de ejecución cuyo comportamiento está determinado por el diseño y no por el descenso del gradiente.
Lo que lo hace especialmente interesante es que ofrece una alternativa al uso de herramientas externas. En lugar de forzar al modelo a abandonar su propio ciclo de ejecución cada vez que se necesita un cálculo exacto, podemos imaginar darle un modo determinista interno. En un régimen, el modelo se comporta como un sistema de lenguaje flexible: genera, abstrae y razona. En otro, se comporta más como una máquina compilada: actualiza el estado, sigue un gráfico de cálculo fijo y ejecuta pasos precisos de manera confiable. Esa es una imagen muy diferente de la pila estándar de “herramientas LLM plus”. Sugiere que al menos algunas formas de cálculo exacto podrían vivir dentro del modelo mismo y no fuera de él.
Un punto de contraste útil es el trabajo reciente de Percepta sobre la ejecución de programas dentro de transformadores. Su construcción compila un mecanismo de ejecución general (efectivamente un intérprete) en los pesos del modelo, al tiempo que proporciona el programa específico en el momento de la inferencia como parte del mensaje. La configuración aquí es más estrecha y especializada. En lugar de poner un intérprete en las pesas, estoy compilando el programa objetivo en las pesas. En otras palabras, su modelo se comporta más como un ejecutor de propósito general para programas suministrados, mientras que éste se comporta más como una máquina compilada dedicada para un gráfico de cálculo fijo. Esto lo hace menos general, pero también más simple y transparente para comprender cómo se puede integrar el cálculo determinista directamente en bloques de transformadores estándar.
En el resto de esta publicación, lo concretaré con un pequeño ejemplo. Comenzaremos con un programa simple, asignaremos sus variables a ranuras de estado oculto y luego analizaremos cómo la atención, las capas de retroalimentación y las actualizaciones residuales se pueden conectar analíticamente para que un transformador estándar lo lleve a cabo paso a paso.
Un pequeño programa que podemos compilar en un transformador
En lugar de permanecer en el nivel de la metáfora, resulta útil observar la ejecución de un programa muy pequeño. Aquí está el programa de juguetes que usaré durante el resto de la publicación:
búsqueda = { "A": 2, "B": 5, "C": 9, } x = entrada y = búsqueda[x] z = y + 1 salida z
Este programa es deliberadamente pequeño, pero contiene los tres ingredientes que necesitamos:
una búsqueda un cálculo local una salida
Supongamos que el token de entrada es B. Entonces el programa debería evolucionar así:
x = B y = búsqueda[B] = 5 z = 5 + 1 = 6 salida 6
La idea clave es que podemos representar este estado de programa en evolución dentro del flujo residual del Transformador. En lugar de tratar el vector oculto como una representación abstracta aprendida, asignamos a partes de él un significado explícito:
ranura 3 = x ranura 4 = y ranura 5 = z
Entonces el estado evoluciona como:
[x = B, y = ·, z = ·] [x = B, y = 5, z = ·] [x = B, y = 5, z = 6]
En ese punto, el cabezal de salida lee z y emite 6. Este es el modelo mental central para el resto del artículo. El flujo residual es el estado de la máquina de un pequeño programa en ejecución. La atención, el cálculo anticipado y la suma residual son los mecanismos mediante los cuales ese estado se lee, actualiza y reescribe.
Un paso de máquina: atención, FFN y reescritura
Una vez que se han asignado las variables a las ranuras, el primer paso no trivial en el programa de juguetes es:
y = búsqueda[x]
De esta manera, reutilizo el mecanismo de atención del Transformer para realizar la operación de búsqueda. En un nivel alto, la atención ya tiene la forma adecuada para este trabajo. Toma un estado actual, lo compara con un conjunto de patrones almacenados, selecciona el relevante y devuelve un valor asociado. En un modelo de lenguaje, ese mecanismo generalmente se usa para la recuperación contextual. Aquí lo uso de una manera mucho más literal: como un pequeño operador de búsqueda determinista.
El cabezal de atención está cableado de modo que:
la consulta representa la solicitud de búsqueda actual, derivada de x las claves representan los casos posibles, como A, B y C los valores representan las salidas correspondientes, como 2, 5 y 9
Entonces, en este ejemplo de juguete, la cabeza se comporta así:
si x == A, devuelve 2 si x == B, devuelve 5 si x == C, devuelve 9
Una vez que se ha seleccionado el valor correcto, todavía tiene que pasar a formar parte del estado de la máquina. Ahí es donde entra en juego la adición de residuos. El bloque de atención no reemplaza directamente todo el flujo residual. En cambio, produce una actualización que dice, de hecho, "escribe 5 en la ranura para y", y la adición residual confirma esa actualización al estado. Después de ese paso, la corriente residual se convierte en:
[x = B, y = 5, z = ·]
La siguiente línea del programa es diferente:
z = y + 1
En este punto, no necesitamos una búsqueda. Ya tenemos el valor que necesitamos. Lo que queda es aplicarle una pequeña transformación local. Ésa es exactamente la función del bloque feed-forward.
En un transformador estándar, el FFN a menudo se describe como una transformación no lineal de posición. Aquí es más útil pensar en ello como una pequeña unidad informática local. Lee el estado actual de la máquina, aplica una transformación fija y produce una actualización que se volverá a escribir en el flujo residual.
Para el programa de juguete, eso significa leer y = 5, calcular z = 6 y emitir una actualización que apunte a la ranura para z. Después de que la adición residual confirma esa actualización, el estado pasa a ser:
[x = B, y = 5, z = 6]
Entonces la división del trabajo es:
Es mejor considerar la atención como búsqueda y enrutamiento. Es mejor considerar el FFN como un cálculo local. La adición residual es el mecanismo de reescritura que confirma la actualización al estado.
Eso da un bucle de ejecución simple:
leer el estado actual calcular una actualización escribir la actualización nuevamente
Esta es la unidad de cálculo dentro del transformador compilado.
De las variables del programa a la compilación
Una vez que el flujo residual se interpreta como el estado de la máquina, la construcción comienza a parecerse mucho menos al diseño de una red neuronal ordinaria y mucho más a una compilación. El cronograma nos dice cuándo debería existir cada variable. En el ejemplo del juguete, x existe primero, luego y se crea a partir de él y luego z se crea a partir de y. En un cálculo más amplio, diferentes variables aparecen en diferentes momentos, permanecen vivas por un tiempo y luego desaparecen cuando ya no son necesarias.
Eso trae consigo la noción de vivacidad. Una variable está viva en un punto determinado del cronograma si algún paso posterior todavía la necesita. Una vez que ningún paso futuro dependa de ello, esa variable estará muerta y su espacio podrá recuperarse.
Esto está muy cerca de la asignación de registros en un compilador tradicional. Un compilador toma variables de programa y decide dónde deben ubicarse en un conjunto limitado de ubicaciones de almacenamiento. Esta construcción hace lo mismo, excepto que las ubicaciones de almacenamiento son ranuras de estado oculto en el transformador. Si una variable muere, su ranura se puede reutilizar posteriormente para otra. Así es como un flujo residual de ancho fijo puede soportar una cadena más larga de cálculos intermedios.
Luego, una capa de transformador estándar se trata como dos pasos de máquina:
una media capa de atención una media capa de FFN
Cada uno lee el flujo residual actual, calcula una actualización y escribe esa actualización mediante la adición de residuos. Entonces, en lugar de pensar en una capa como una transformación opaca, es más útil pensar en ella como dos transiciones de estado explícitas.
Es por eso que el programa opera con una granularidad más fina que las capas completas. Le importa qué variables están vivas después de la parte de atención de una capa y después de la parte FFN de una capa. El nacimiento, la muerte, la reutilización y la reescritura pueden ocurrir en ese nivel.
Una vez tomadas esas decisiones, la tarea restante es convertir esa estructura compilada en parámetros reales del transformador. En otras palabras: dado un gráfico de cálculo, un cronograma y una asignación de espacios, ¿cómo construimos los pesos de incrustación, atención, FFN y salida que hacen que el modelo ejecute el programa?
Del gráfico de cálculo a los pesos
En este punto, hemos definido todas las piezas conceptuales clave que nos permiten convertir un transformador en una computadora de propósito general:
un programa define el cálculo deseado un cronograma decide cuándo se debe calcular cada intermedio una asignación de ranura decide dónde reside cada variable en la atención del flujo residual y las medias capas de FFN proporcionan la maquinaria de transición de estado
El paso restante es convertir esa estructura en pesas para transformadores.
El punto de partida es el gráfico de cálculo. Cada cantidad intermedia en el programa se representa como una dimensión simbólica junto con la expresión que define cómo se debe calcular a partir de cantidades anteriores. Luego, el programa decide qué cálculos ocurren en cada media capa de atención y cuáles ocurren en cada media capa de FFN. También especifica cuándo cada variable se activa, cuándo deja de estar activa y cuándo se puede reutilizar su ranura. Una vez que se fija ese cronograma, cada variable activa se asigna a una ranura de estado oculto. Algunas ranuras se fijan desde el principio, otras se asignan cuando nacen nuevas variables y otras se reutilizan más tarde cuando las variables anteriores mueren.
Existe una distinción importante que vale la pena mencionar aquí entre la colocación global y la construcción de peso local. Convertir una expresión simbólica en un vector indexado por ranura es el paso local: una vez que sé dónde viven x, y y z, convertir 3·x — 2·y + z en un vector disperso es casi mecánico. El problema más difícil llega antes. Antes de poder escribir pesos, primero tengo que decidir dónde se ejecuta cada operación en el transformador finito, qué ranura ocupa cada intermedio y cuándo se puede reutilizar una ranura de forma segura.
Es por eso que esto comienza a parecer un problema de backend del compilador. El sistema tiene que satisfacer varias restricciones a la vez: las búsquedas deben realizarse en fases de atención, los cálculos locales no lineales deben realizarse en fases FFN, cada consumidor debe correr detrás de su productor y el número total de capas y espacios de flujo residual debe ser lo más pequeño posible. Como lo describe Percepta, en construcciones más generales, esto puede formularse como un problema de programación y asignación de registros y resolverse con un programa de números enteros mixtos. Una vez que se resuelve el problema de la ubicación global, la construcción del peso restante se vuelve mucho más simple.
Sólo entonces construimos realmente las pesas. El mecanismo principal es convertir expresiones simbólicas sobre variables de programa en vectores sobre ranuras. Supongamos que una expresión simbólica es
3·x – 2·y + z
y supongamos que x, y y z han sido asignados a las ranuras 3, 4 y 5 respectivamente. Entonces la expresión se convierte en un vector que es cero en todas partes excepto en esas tres posiciones, donde sus entradas son 3, -2 y 1. De hecho, el compilador ha reemplazado los nombres de las variables con direcciones de ranura. La expresión simbólica se ha convertido en una lectura lineal concreta del estado actual de la máquina.
Una forma sencilla de imaginarlo es:
ranura 0 ranura 1 ranura 2 ranura 3 ranura 4 ranura 5 … 0 0 0 3 -2 1
Entonces, la relación es sencilla: la expresión simbólica dice qué combinación de variables se necesita, y el vector de ranura es la implementación a nivel de transformador de esa misma expresión en coordenadas de estado oculto.
La pregunta restante es dónde debería ir ese vector dentro del transformador. Eso depende del papel que desempeña la expresión en el gráfico de cálculo:
Si la expresión define cómo se debe inicializar el estado a partir de un token de entrada, se incluye en una fila de incrustación. Si se utiliza para decidir qué caso debe coincidir durante una búsqueda, se coloca en una fila de consulta o en una fila clave. Si especifica el resultado que se debe devolver una vez que se coincide con un caso, ingresa en una fila de valor. Si define una transformación determinista local sobre el estado actual, ingresa en una fila de ponderación FFN. Si define lo que se debe emitir desde el estado final de la máquina, ingresa a una fila de cabezal de salida.
Entonces el compilador realmente está haciendo dos cosas a la vez. Primero, traduce la lógica del programa simbólico en vectores indexados por ranura. Luego coloca esos vectores en el bloque transformador que implementa la operación requerida.
Ese es el puente clave entre los programas y los transformadores. Una expresión simbólica sobre variables nombradas se convierte en un tensor concreto sobre dimensiones de estado oculto, y su papel en el gráfico de cálculo determina si se utiliza para la inicialización, coincidencia, recuperación, cálculo local o salida del estado. Una vez que la traducción esté en su lugar, podemos ir bloque por bloque y cablear el transformador para ejecutar el programa.
Visto así, la construcción está haciendo algo bastante inusual. Al reutilizar los bloques centrales del transformador de la manera que lo hemos hecho, estamos convirtiendo un sistema probabilístico de reconocimiento de patrones en una máquina determinista. En un modelo entrenado, los circuitos internos suelen ser algo que inferimos después del hecho: observamos el comportamiento, inspeccionamos las activaciones y tratamos de adivinar qué circuito surgió. Aquí el circuito se conoce de antemano. Las pesas están diseñadas para realizarlo deliberadamente. Eso hace que sea mucho más fácil razonar sobre el modelo, porque la función computacional de cada bloque es explícita desde el principio.
Escalar la ejecución del programa a rastreos deterministas largos
La pregunta restante es qué sucede cuando esta idea se lleva más allá, en particular, cómo la ejecución con muchas búsquedas puede hacerse lo suficientemente eficiente como para soportar seguimientos deterministas mucho más largos.
En las construcciones anteriores, la atención suele operar en un régimen de búsqueda casi estricto. Conceptualmente, un encabezado forma una consulta q, la compara con las claves candidatas k₁, k₂,…, kₙ, calcula las puntuaciones q · kⱼ y devuelve el valor adjunto a la clave con la puntuación más alta. En una implementación estándar, eso significa escanear todas las claves candidatas. A medida que crece la traza, esto se vuelve cada vez más caro.
La observación útil es que esta búsqueda se puede reinterpretar geométricamente. La consulta q define una dirección, cada clave es un punto y la clave seleccionada es aquella cuyo producto escalar con q es mayor; en otras palabras, el punto más lejano en esa dirección. Si las claves viven en dos dimensiones, eso es exactamente un problema de búsqueda de casco convexo.
Aquí es donde aparece la ventaja computacional. Una búsqueda de atención lineal verifica cada clave candidata. En la vista de casco convexo, los puntos interiores se pueden descartar porque nunca pueden ser maximizadores en ninguna dirección de consulta. Eso significa que podemos mantener una estructura de datos de casco convexo sobre las claves 2D almacenadas y responder a cada búsqueda buscando esa estructura, en lugar de escanear el conjunto completo de claves anteriores.
La misma reformulación geométrica existe también en dimensiones superiores. En dos dimensiones, el conjunto de claves potencialmente ganadoras se encuentra en un límite poligonal simple, y las consultas sobre puntos de apoyo pueden responderse eficientemente en ese límite. Sin embargo, en dimensiones superiores, el casco se convierte en un politopo mucho más complicado con muchas caras y muchos más puntos extremos. En el peor de los casos, una gran fracción de las claves almacenadas pueden reposar en el propio casco, por lo que la ventaja del "pequeño límite" desaparece. Una vez que eso sucede, buscar en el casco ya no es mucho más barato que escanear el conjunto de claves original, y la aceleración geométrica efectivamente se degrada hacia el escaneo lineal. Es por eso que esta idea es interesante para dimensiones de cabeza muy pequeñas, especialmente 2D, pero no es una ruta práctica hacia la atención sublineal para transformadores estándar con cabezas grandes: a medida que la dimensión de la cabeza crece, el casco deja de ser un resumen compacto del conjunto candidato y la ventaja computacional se evapora en gran medida.
Hacer que todo esto sea determinista en la práctica
Por supuesto, existe una brecha entre la construcción de juguete limpia que hemos construido en este artículo y un sistema práctico. Para que el cálculo compilado dentro de los transformadores funcione de manera confiable en el mundo real, se deben alinear varias cosas: la transición de estado prevista debe calificarse por encima de todas las alternativas en cada paso, la reutilización de ranuras debe administrarse cuidadosamente y el problema de programación debe ajustarse a un presupuesto finito de ancho y profundidad. El determinismo aquí no es algo que los transformadores proporcionen gratis; debe integrarse en la construcción mediante un diseño de peso exacto, una ubicación cuidadosa de las operaciones y una regla de decodificación fija.
El trabajo de Percepta es notable porque aborda estas limitaciones prácticas de principio a fin. Definen primitivas nativas del transformador, las compilan en un gráfico de puerta, usan optimización de enteros mixtos para programar operaciones y reutilizar ranuras de flujo residual, y luego construyen analíticamente los pesos. Fundamentalmente, también introducen una metodología de decodificación adaptada a los seguimientos de ejecución, por lo que el modelo se comporta menos como un predictor probabilístico ordinario del siguiente token y más como un ejecutor controlado cuyo siguiente paso previsto gana de manera confiable bajo una decodificación codiciosa. Combinado con construcciones de búsqueda exactas y un intérprete integrado en los pesos, eso convierte la idea de una posibilidad teórica interesante en algo mucho más cercano a un sistema de ejecución funcional.
Un nuevo patrón de diseño de IA: integración de representaciones aprendidas con algoritmos deterministas
La compilación de programas en pesos de transformadores apunta a un futuro atractivo para los sistemas de IA: arquitecturas que puedan combinar la inferencia probabilística con el cálculo determinista dentro del mismo modelo, en lugar de entregar constantemente el trabajo exacto a herramientas externas. Los componentes probabilísticos se adaptan bien a la abstracción, el reconocimiento de patrones y el razonamiento en condiciones de incertidumbre. Los componentes deterministas son muy adecuados para actualizaciones de estado precisas, ejecución exacta y cadenas de cálculo largas y confiables. Los sistemas reales necesitan ambos.
La promesa más profunda es que la ejecución determinista no tiene por qué permanecer fuera del modelo. Si las operaciones nativas del transformador, como los bloques de atención y retroalimentación, se pueden organizar en un sustrato de ejecución programable, entonces partes de un intérprete o un programa especializado se pueden compilar directamente en los pesos. El modelo ya no se limita a llamar a las herramientas de forma más inteligente; está empezando a internalizar parte del cálculo mismo.
Esto apunta a un tipo de máquina más unificada: una que pueda razonar con flexibilidad cuando la incertidumbre es inevitable y pasar a una ejecución exacta cuando la precisión importa. Para dominios de alto riesgo donde tanto la confiabilidad como el desempeño son importantes (finanzas, atención médica, seguros y cadenas de suministro, entre ellos) esto se siente como un importante patrón de diseño de IA emergente.
Y la buena noticia es que ya puedes probar esto en tus propios sistemas de IA: Percepta ha desarrollado un sistema que compila analíticamente una máquina virtual WebAssembly en pesos transformadores para que los programas compilados puedan ejecutarse paso a paso dentro del propio modelo. Intenté transmitir las ideas principales del enfoque de Percepta a través de un pequeño ejemplo compilado en este artículo. La conclusión clave del trabajo de Percepta es que el cálculo determinista no siempre tiene que quedar fuera del modelo como una herramienta separada. En algunos casos, se puede compilar en el propio circuito del modelo.
Descargo de responsabilidad: Los puntos de vista y opiniones expresados en este artículo son míos y no representan los de mi empleador ni los de ninguna organización afiliada. El contenido se basa en la experiencia y la reflexión personal y no debe tomarse como consejo profesional o académico.
Inspirado en los artículos de Percepta: ¿Pueden los LLM ser computadoras? y construcción de una computadora LLM
📚 Lecturas adicionales
transformador-vm: contiene la maquinaria similar a un compilador descrita en este artículo: gráficos de cálculo, cronogramas, asignación de ranuras y construcción analítica de los pesos de los transformadores. Publicaciones de blog de Percepta (2026): Percepta (2026) – ¿Pueden los LLM ser computadoras? Percepta (2026) – Cómo construir una computadora LLM explora cómo los transformadores se pueden convertir en una especie de "computadora LLM" interna que ejecuta largos cálculos deterministas en lugar de limitarse a hacer coincidir patrones con los siguientes tokens. Muestra cómo se puede incorporar un comportamiento procesal exacto dentro del modelo, permitiendo una ejecución confiable de varios pasos sin el uso constante de herramientas externas. También examina cómo la atención 2D da lugar a mecanismos de búsqueda geométrica, incluidas estructuras que admiten operaciones como consultas de casco convexo.