Pular para o conteúdo

Curso Jev – Inferência somente com prefill e decision models do zero

Ver no idioma original →

Autoria: freeCodeCamp.org · Assistir no YouTube

Vídeo de autoria do canal indicado, não da IANIA; todos os direitos pertencem ao criador. Transcrição, resumo e tradução gerados automaticamente; o conteúdo no idioma original é preservado na versão em idioma original.

Idioma original: inglês

Vídeo original: Jev Course – Prefill-Only Inference & Decision Models from Scratch · Canal: freeCodeCamp.org · 1:25:49 · Assistir no YouTube

Tópicos: Inferência e otimização de modelos, Modelos de IA

O vídeo ensina a formular decision models no estilo Jev usando modelos pré-treinados e PyTorch, substituindo a geração token a token por uma única passagem de prefill. Compara abordagens baseadas em encoder e decoder e mostra como obter probabilidades para escolhas, categorias ordinais e respostas binárias. Também apresenta os princípios de treinamento com RLCD, incluindo teacher-student distillation, GRPO e funções de recompensa. Por fim, discute limitações e trade-offs de cada arquitetura para diferentes tarefas.

  • Jev evita a etapa de decode e produz decisões tipadas em uma única passagem forward, com latência previsível.
  • As interfaces discutidas são choice para opções nominais, score para categorias ordinais e null para respostas binárias.
  • Na inferência convencional, o LLM prevê um token por vez e reutiliza o KV cache durante a geração.
  • Uma implementação com encoder combina a representação da pergunta com representações das opções, mas precisa reprocessar o contexto para cada pergunta.
  • Uma implementação com decoder pode pré-calcular e reutilizar o KV cache do contexto em várias perguntas.
  • Ao agrupar várias perguntas com um decoder, a máscara de atenção deve impedir que uma pergunta acesse os tokens de outra.
  • Opções que começam com o mesmo token podem ser ambíguas na abordagem de primeiro token; renomeá-las ou pontuar spans completos são alternativas.
  • O treinamento com RLCD pode usar distribuições de um modelo teacher, perturbações aleatórias nas pontuações e GRPO para orientar o modelo student.
  • A escolha entre encoder e decoder depende, entre outros fatores, da reutilização do contexto, do tamanho das opções, do número de perguntas e dos recursos disponíveis.
Início Capítulo Resumo
00:00 Visão geral de Jev e RLCD O vídeo apresenta decision models que substituem a geração token a token por uma única passagem de prefill.
06:46 Plano do curso e inferência convencional O instrutor define as etapas do curso e introduz o fluxo tradicional de geração de um LLM.
13:06 Decode, logits e KV cache A demonstração acompanha a seleção de tokens a partir dos logits e explica como o cache é atualizado.
21:30 Abordagem com encoder O vídeo apresenta uma formulação encoder-only que usa perguntas, opções, tokens de máscara e contexto.
25:36 Construção da entrada do encoder A implementação organiza os tokens da pergunta e das opções antes de acrescentar o contexto.
35:01 Representações e probabilidades no encoder As representações da pergunta e das opções interagem para produzir pontuações que podem ser normalizadas como probabilidades.
40:50 Abordagem com decoder e opções O instrutor explica como o decoder pode reutilizar o cache do contexto e apresenta formas de representar as opções.
46:35 Máscara causal para várias perguntas Um exemplo mostra como permitir que cada pergunta acesse o contexto sem consultar as demais perguntas.
52:19 Implementação do decoder e pré-cálculo do contexto A demonstração prepara o KV cache do contexto e organiza as perguntas e opções para inferência.
56:24 Construção da máscara de atenção O vídeo constrói uma máscara que separa os tokens de cada pergunta e restringe a atenção entre perguntas.
1:02 Inferência e probabilidades com decoder O modelo calcula probabilidades para as opções usando as posições de decisão e o contexto já armazenado.
1:09 Limitações e treinamento por distillation O instrutor discute colisões entre opções e introduz o treinamento student-teacher com GRPO.
1:16 Funções de recompensa e loss do RLCD O vídeo explica log score, spherical score e RPS como componentes para orientar a calibração das previsões.
1:23 Comparação entre encoder e decoder A conclusão compara os trade-offs das arquiteturas e indica fatores para escolher entre elas.

A latência fica completamente sob controle, e a saída se torna previsível. — 02:35

Uma passagem forward, sem decode, e resultados previsíveis. — 03:07

Posso continuar trocando as perguntas aqui e reutilizar o KV cache. — 41:21

O estado nunca muda; ele já foi armazenado em cache. — 1:08

Se você tem muitas perguntas sobre um contexto finito, acho que o decoder é a abordagem certa. — 1:23

  • Jev (conceito): Paradigma de inferência que substitui a geração token a token por decisões tipadas obtidas sem a etapa de decode.
  • RLCD (conceito): Abordagem de treinamento apresentada para produzir decisões calibradas, usando sinais de recompensa e distribuições de referência.
  • prefill (conceito): Etapa em que o modelo processa os tokens de entrada antes de gerar uma resposta.
  • KV cache (conceito): Cache de keys e values da atenção, reutilizável para evitar recalcular o contexto em novas perguntas com um decoder.
  • GRPO (conceito): Método de otimização por reforço apresentado para comparar trajetórias e usar vantagens relativas na construção da loss.
  • RPS (Ranking Probability Score) (conceito): Métrica descrita para avaliar previsões ordinais, considerando a distância entre probabilidades acumuladas.
  • PyTorch (ferramenta): Biblioteca usada no vídeo para implementar e inspecionar os modelos e as operações de inferência.
  • Hugging Face (empresa): Plataforma cujas bibliotecas e modelos pré-treinados são usados como base para as demonstrações.
  • BERT (modelo): Exemplo de modelo encoder-only usado para explicar inferência com tokens de máscara.
  • Qwen (modelo): Modelo decoder-only usado na demonstração de inferência com KV cache.
  • Clef (ferramenta): Implementação alternativa de Jev mencionada no vídeo e associada a uma abordagem de pooled span.
  • Cloudflare (empresa): Empresa citada em relação à implementação Clef.
  • Vivek (pessoa): Nome usado em um exemplo para ilustrar a máscara causal e o agrupamento de perguntas.

00:00 Aprenda a criar e dominar decision models no estilo Jev do zero usando PyTorch e backbones do Hugging Face. Ao substituir a decodificação autorregressiva token por token por uma única passagem forward de pré-preenchimento, Jev elimina completamente a geração de texto para fornecer decisões tipadas, limitadas e de baixa latência, como escolhas, pontuações ordinais e valores booleanos nulos. Você aprenderá sobre paradigmas de inference e explorará workflows de treinamento

00:30 com RLCD para produzir probabilidades honestas e bem calibradas para a lógica de software do mundo real. >> Olá e boas-vindas a este aprendizado de Jev do zero usando apenas PyTorch e bibliotecas básicas como Hugging Face. Usaremos modelos pré-treinados como backbone, mas tentaremos aprender os princípios fundamentais de inference e parte do treinamento do zero, para que você tenha uma ideia de

01:01 como funciona esse paradigma específico de LLMs. Então, neste vídeo, abordaremos Jev e RLCD, aprendizado por reforço para decisões calibradas. Espero que gostem. A primeira coisa que queremos abordar é: o que é Jev e o que é RLCD? Provavelmente chegaremos ao RLCD mais para o fim desta série específica, porque quero muito abordar primeiro a inference e os diferentes paradigmas pelos quais é possível fazer inference, e depois voltar para entender

01:33 os workflows de treinamento. Então, em essência, Jev é muito simples. Se você entende como funciona um LLM, um LLM tem dois estágios: pré-preenchimento e decodificação. O pré-preenchimento é o estágio em que ele processa todo o prompt, tem natureza limitada por computação e está prestes a gerar o primeiro token. A decodificação é a geração da resposta, um token de cada vez. Se você inserir 500 palavras como tokens para dar contexto a uma

02:04 tarefa específica e o modelo estiver tentando gerar a resposta, o primeiro token é gerado e, em seguida, esse primeiro token é enviado de volta, junto com mais 501 tokens, e o modelo produz o token 502. Todos os 502 tokens gerados até então são enviados de volta, e o token 503 é produzido, e assim por diante. O que Jev tenta fazer é pular completamente a parte de decodificação e condensar o enunciado do problema na

02:35 forma de uma única passagem forward, ou somente de um enunciado pré-preenchido. Isso obviamente tem muitas vantagens. Para começar, sua latência ficará totalmente sob controle, mas a saída também será previsível. Isso significa que você especifica de antemão o que quer do modelo, na forma de opções ou valores booleanos, e ele retorna exatamente o que você procura, em vez de você

03:07 se atrapalhar com sintaxe e linguagem tentando tornar o LLM previsível. Então, são duas coisas: uma passagem forward, sem decodificação, e resultados previsíveis. Essas são as duas propriedades desejáveis de Jev, por assim dizer. Agora, quanto à interface de Jev, você pode fazer perguntas de três maneiras. A primeira é a escolha

03:38 em que você pode fazer algo parecido com uma pergunta de múltipla escolha. Você pode ter a opção A, a opção B ou a opção C. Uma pontuação faz o mesmo, mas tem natureza ordinal. Então, um bom exemplo é este: você tem opções nominais e, depois, a qualidade delas é representada por opções ordinais. O que isso quer dizer? Se você pedir direções, leste, oeste, norte e sul são de natureza

04:09 nominal. Leste não é maior que oeste, nem maior que sul, e assim por diante. Portanto, não há uma ordem que se possa manter entre as escolhas. As escolhas simplesmente existem de forma ortogonal. Não podem ser colocadas em uma determinada ordem. Já as opções ordinais podem ser ordenadas. Qual é o sentimento do cliente? Veja: ele está calmo, frustrado ou muito irritado. Então, elas podem ser ordenadas pelo nível de insatisfação, por assim dizer. As APIs expostas por Jev para escolha

04:40 e pontuação são diferentes. E, por fim, há o valor nulo, que é simplesmente binário. Você faz uma pergunta de sim ou não e recebe as probabilidades correspondentes. A formulação é interessante. Então, o nominal é um pouco diferente do ordinal, que é bem diferente do binário. Vamos tentar criar uma estrutura que atenda a todos eles, mas também vamos reconhecer e respeitar as nuances dessas três APIs.

05:10 Vamos tentar formar um backbone comum para tudo o que fazemos e, a partir dele, construir as camadas necessárias para separar as nuances. Para nosso caso de uso, criaremos os três usando um softmax por pergunta, mas com certeza vou explicar onde eles podem diferir. Agora, vamos falar sobre alguns pré-requisitos para adotar isso. Não é para todo mundo. Então, se você tem um bom domínio

05:42 dos conceitos de attention, de como funciona o KV cache, do que é uma lógica e do que é um softmax, e tem fluência em PyTorch, acho que este conteúdo pode ser para você. Se você não conhece algum desses assuntos, vai achar este vídeo um pouco difícil de acompanhar. Algumas coisas são úteis, mas não obrigatórias: ter um pouco de conhecimento de Hugging Face e saber usar um debugger. Usaremos o debugger do VS Code para percorrer linha por linha alguns dos

06:13 conceitos que tentaremos aprender. Também é muito bom conhecer alguns aspectos modernos de transformers, como encoder, decoder e assim por diante. Portanto, estes são pré-requisitos realmente obrigatórios. Se você não conhece esses assuntos, provavelmente este vídeo não vai agregar muito valor para você. E, se também os conhece, ótimo, mas isso não é obrigatório. Certo. O que não é obrigatório, porém, é ter uma GPU. Por isso,

06:46 a programação deste curso inteiro será bastante focada em inference. Falaremos bastante sobre a teoria, mas, nas implementações, vamos nos limitar à inference. Portanto, você não precisa de uma GPU para isso. Você não precisa ter experiência com treinamento, treinamento de modelos ou qualquer tipo de matemática além de entender a função softmax. Só isso. Então, vamos começar. Eis o que faremos. Vamos dividir tudo em três fases. Na primeira fase, veremos a geração básica

07:18 com LLMs. Vamos tentar escrever um prompt que gere um JSON token por token. Nesta fase, veremos juntos os estágios de pré-preenchimento e decodificação. Isso dará uma boa intuição sobre o problema e contextualizará tudo para continuarmos. Depois, veremos uma implementação somente com encoder. Em seguida, veremos a implementação somente com decoder. Vamos voltar a uma execução de treinamento para entender como funciona e depois retornar para, finalmente, fazer

07:51 uma comparação entre as abordagens e discutir quando usar cada uma. Agora, antes de entrarmos em como Jev realmente funciona, acho que precisamos revisar o paradigma de decodificação básico, ou a maneira como normalmente um LLM faz inference. Isso vai colocar as coisas em perspectiva quanto ao rumo que estamos tomando e a como reformular tudo para refletir o comportamento que Jev espera de você. Então, temos aqui o tokenizer e o

08:22 modelo que estamos carregando logo de cara. Mais uma vez, estamos usando LLMs pré-treinados. Usaremos LLMs pré-treinados durante todo o curso. Neste caso, se eu olhar a função de carregamento, o modelo aqui é um quant 3.6B. Ele é pequeno o suficiente para caber na maioria dos laptops. Então, temos o modelo carregado aqui. E agora vamos ver como é um prompt ingênuo. Temos o tokenizer aqui. O contexto

08:53 é assim. O cartão terminado em 4412 fez nove compras em 3 minutos, em quatro países. O titular do cartão ligou para dizer que o cartão ainda está no bolso dele. Parece uma transação fraudulenta. E o schema que você vai usar é mais ou menos assim. Normalmente, este será nosso objeto Pydantic, que você passará a algum provedor de LLM, como uma entidade que oferece uma API da OpenAI. O schema contém

09:25 algo chamado priority. Vamos dar uma olhada nisso. É um enum cujas opções são alta e baixa. Depois, contém mais alguns itens: decision, cujas opções são novamente fraud, fraud review ou clear; e escalate. Você vê que esses três foram escolhidos cuidadosamente para representar as três interfaces que Jev expõe. Por um lado, temos priority,

09:57 que pode ser alto ou baixo. Essa é a API de score do Jev. Há a decisão, que é novamente subjetiva. E então, a partir daí, há o booleano. Então, estas são as três perguntas que tentei formular, que imitam todas as interfaces que o Jev expõe. Depois, vamos tentar condensar isso em exemplos mais simples para ver como o Jev funciona mais adiante, mas primeiro vamos descobrir o que isso faz. Então, estamos passando por uma função de prompt ingênua

10:29 que simplesmente pega tudo isso e coloca em um formato pronto para inference. Vou pular várias dessas etapas de manipulação de dados, porque não são tão interessantes. Quando esse processador retornar, vamos dar uma olhada na saída e poderemos ver claramente algumas das coisas que foram feitas nos bastidores

11:00 nisso. Então, vamos tentar limpar isso. Se eu olhar para o prompt, tentei colocá-lo em um formato do tipo que os provedores de LLM preferem. Se eu imprimir isso, há um token IM start. E então começa a mensagem do sistema: “você é um sistema preciso de extração de dados”, blá-blá-blá. Depois, há um esquema JSON ao qual quero que você obedeça. E então o usuário fornece o contexto. Então, isso é

11:31 o que é chamado de estado no Gemma, e este é o prompt do usuário, que será colocado no estado; depois, as perguntas virão. Certo? Então, neste caso, em uma decodificação padrão de LLM, tudo o que estou pedindo é: aqui está o prompt do usuário, aqui está o system prompt, seguido pelo prompt do usuário. Você pode me devolver o JSON, por favor? Certo? Então, até abrimos essa chave para que o LLM possa continuar a decodificação com precisão.

12:02 Certo. Fizemos muitas coisas para transformar os dados no formato correto, mas elas não eram realmente de interesse para nós na nossa busca por entender o Jev, então pulei várias dessas etapas de processamento. Neste caso, tudo o que estamos fazendo é tokenizar o prompt inteiro. Então, você vai obter um monte de trechos que especificam todos os IDs de token correspondentes aos tokens. Certo?

12:34 Então, vamos avançar. Se houver um token de fim de sequência, ótimo; caso contrário, definiremos o nosso próprio, e este será novamente uma chave JSON sem escape, para indicar que estamos tentando concluir a decodificação com um JSON gerado. De novo, eu diria que tudo isso é só trabalho burocrático. É aqui que a decodificação propriamente dita começa. Vamos tentar limpar a área de trabalho para entender isso. Vamos olhar mais de perto.

13:06 Então, nossas entradas, os inputs tokenizados, passam pelo modelo, que então produz uma saída. Vamos pegar os logits, extrair os valores de past key, que se tornarão nosso cache KV, e então vamos olhar para o último token, percorrer seu vocabulário e extrair o argmax, que basicamente é o

13:36 token com a maior probabilidade. Agora, se ele corresponder a uma das nossas sequências de parada, vamos interromper. Caso contrário, vamos continuar, e ficará bem claro quando eu gerar isso e mostrar passo a passo o que está acontecendo. Então, se pegarmos o modelo, passarmos as entradas por ele e, em todos os momentos, fornecermos o cache. Neste caso, o cache está definido como none, mas depois vocês verão que o

14:07 cache é atualizado. Então, o que podemos fazer aqui é, se tivermos o mesmo estado ou o mesmo contexto e quisermos responder a várias perguntas subsequentes, podemos armazenar em cache o estado do contexto. Depois, quando eu fizer a primeira pergunta, posso simplesmente recuperar esse cache e processá-lo concatenando a pergunta com aquele contexto específico. Se eu tiver uma

14:39 segunda pergunta para fazer, posso simplesmente pegar o contexto do cache novamente, anexar a segunda pergunta e continuar fazendo isso, e assim por diante. Então, estou apenas mantendo um cache, o que é uma boa prática. Certo. A partir daí, os logits são gerados. Só para dar uma ideia, eu tinha… deixa eu olhar as entradas. Temos os IDs de entrada, que me dão uma ideia do número de tokens.

15:12 Então, se eu pegar o comprimento disso, obtenho um, porque é um JSON aninhado. Quer dizer, é um array aninhado, desculpem. Então, há cerca de 130 tokens no total em tudo isso agora. Certo. A partir daí, quando a saída estiver pronta, vocês verão que, se eu acessar

15:43 os logits, terei 130 saídas, e cada posição de token terá um número de opções igual ao tamanho do vocabulário. Então, a maneira como o transformer funciona é: se você inserir cinco tokens, obterá cinco previsões. Cada previsão contém toda a distribuição de probabilidade do vocabulário em questão. Então, se você estiver procurando apenas o último token, o que faz é descartar

16:14 tudo, exceto a última previsão, e então usar essa distribuição de probabilidade para extrair o argmax, que é exatamente o que estamos fazendo aqui. Então, estamos atualizando o cache. Agora, pegamos as primeiras dimensões desses logits. Como estamos processando apenas um lote, se pegarmos só o primeiro e verificarmos sua forma, vocês vão notar que o um que estava na frente desapareceu. E o que resta é 130 por

16:46 152 mil, o que queremos examinar. E então, se eu pegar o último elemento disso, fico essencialmente com a distribuição de probabilidade das previsões do último token. Se eu verificar a forma disso, tenho 152 mil saídas com que lidar. A partir daí, só quero convertê-las para float, passá-las por um softmax, e esta é apenas a dimensão menos um.

17:17 Então, ele aplica um softmax a todos esses números. E depois vou pegar o argmax. Se eu realmente pegar isso e fizer tudo sozinho, um por um, vou obter um tensor com 152 mil probabilidades, aproximadamente. E então, se extrair o argmax delas, obtenho o ID de token do token que tem a maior

17:48 probabilidade. Então, o argmax dá a posição, que nada mais é do que o ID de token. Posso decodificar isso e obter minha resposta. Vamos ver. É 330. Então, este é o token que… Então, este é o token previsto neste loop. Se eu deixar isso rodar por algumas iterações e, de novo,

18:19 verificar o token, o token recém-gerado, vocês verão que ele continuará mudando com o tempo. Agora, ele se torna “priority”. Então, acho que provavelmente teremos a aspa invertida fechando junto com os dois-pontos. E, se continuarmos fazendo isso por algumas iterações,

18:50 vocês verão que o acúmulo que está acontecendo nesta variável grown vai refletir a resposta que estamos procurando. É exatamente por isso que vocês veem o ChatGPT, na interface web, fornecer tokens um por um. Isso acontece porque ele percorre todo o contexto anterior e gera esses tokens um por um. Então, ele considera alta a prioridade deste chamado específico ou deste contexto específico.

19:21 E então, se continuarmos observando enquanto iteramos por algumas rodadas, vocês verão que ele chegará, aos poucos, a um ponto em que terá gerado quase todo o JSON. Então, se eu… se eu olhar para isso de novo, ele terminou de prever o segundo campo. Ele considera que é uma fraude. E então ficamos com “escalate”.

19:54 ainda tem mais tokens para gerar. E, se chegarmos ao fim, depois que ele terminar o loop, poderemos ver o texto final previsto após todas as iterações de geração desses tokens, um por um. Se olharmos para o texto aqui, vocês verão que ele retornou o JSON final. Vamos imprimi-lo para que fique bem claro. Então, vamos

20:25 Vamos imprimir o texto para que ele seja formatado de maneira legível. Então, dá para ver que, desde esta aspa específica até a palavra “prioridade”, e depois escolhendo outro token para esse texto específico e gerando token por token, isso levou muito tempo. Esse tempo poderia ter sido completamente evitado se tivéssemos formulado o enunciado do problema de maneira inteligente, permitindo apenas pressupostos previsíveis, sem

20:58 entrar nesse ciclo vicioso de decodificação, pelo menos para este enunciado do problema. Certo. Há um monte de tarefas de organização que cuidam de garantir que isso termine gerando um JSON, verificando os caracteres de escape, o início, o fim etc. E então, finalmente, quando o pós-processamento termina, sobra uma resposta. Agora, vimos como um modelo somente decodificador foi usado

21:30 para gerar uma resposta. Agora vamos começar com uma das primeiras formulações do nosso modelo generativo. E isso será feito por meio do backbone de um encoder. O que faremos é tentar aproveitar um modelo encoder, como BERT ou qualquer outra variante dele, para reformular o caso de uso de modo que possamos evitar totalmente a etapa de decodificação e ainda ver se conseguimos obter

22:01 tudo o que queremos receber de volta. Então, antes de mais nada, vamos usar um modelo somente encoder. Não faremos nenhum treinamento. E tentaremos ver até que ponto conseguimos adaptá-lo a este caso de uso específico. E, se não der certo, quais abordagens de treinamento podem ser usadas. Certo. A primeira coisa que vocês verão é que vamos codificar a pergunta aqui primeiro.

22:31 Agora, essa é uma abordagem bem peculiar. Normalmente, você fornece o contexto e depois faz a pergunta. Aqui, estamos fazendo o contrário. Começamos com a pergunta e depois fornecemos o contexto aqui. Isso acontece porque, tipicamente, a atenção dá bastante importância ao início dos tokens no bloco de atenção. Então, queremos nos certificar de colocar isso no começo. Acho que já foram feitos experimentos com a ordem inversa, e essa abordagem acabou tendo um desempenho melhor. Então, vamos

23:02 manter esta implementação. Agora, depois que a pergunta é feita, temos todas essas opções. No nosso caso, eram alta ou baixa, fraude ou análise, ou até sim ou não. Neste caso, antes de cada opção, teremos um token mask. Esse é um tipo especial de token que vamos inserir. E vamos organizar tudo em sequência. Vamos coordenar o processo inteiro. Depois, vamos usar essa previsão para avaliar qual opção podemos

23:34 prever. Então, se você tiver a opção B, vamos inserir um token mask junto dela, ou logo antes da opção B. Se tiver a opção C, vamos inserir um token mask antes dela, e assim por diante. E, por fim, teremos todo o contexto aqui. Ao ver isso, fica claro que teremos que fazer isso para cada pergunta. Então, se você tiver um contexto muito longo aqui, toda vez que fizer uma nova pergunta, será preciso inserir novamente o estado inteiro nesse

24:04 processo. Então, você pode perguntar: “É um modelo encoder. Por que o estado não pode vir primeiro? Por que não posso armazená-lo em cache? E por que não posso simplesmente trocar as perguntas, uma após a outra?” Bem, aqui está o problema. Os encoders são bidirecionais por natureza. Então, toda vez que você muda a pergunta, acaba alterando completamente os pesos de atenção em cada

24:35 posição da sua matriz de atenção. Por isso, mesmo que você inverta essa ordem e coloque o estado primeiro e a pergunta por último, toda vez que mudar a pergunta, os estados bidirecionais — os pesos — vão mudar, e você terá que recalcular tudo do zero, repetidas vezes. Espero que dê para perceber. É uma interface muito simples, mas dá para ver claramente que, para cada pergunta feita, será preciso continuar acrescentando o estado de novo e de novo. Então, se você tiver muitos tokens

25:06 e muitas perguntas para fazer, talvez isso não seja muito escalável. Mas, como a atenção é bidirecional, ela vai proporcionar o máximo retorno para cada parâmetro do modelo. Certo? Então, sem mais demora, vamos entrar na implementação, que está toda em encoding.py. E vamos definir um breakpoint aqui, para testar.

25:36 O que nos interessa é ter um estado. Vamos usar duas perguntas só para ver como ele interage com a configuração de várias perguntas. A partir daí, veremos como estamos incorporando a pergunta, o mask e o contexto, tudo junto. Uma coisa que evitei deliberadamente foi tentar agrupar as perguntas, embora seja possível. Em vez disso, voltei ao método tradicional de usar loops for, para podermos analisar cada parte e ver como isso funciona. Esta não é a

26:08 implementação mais eficiente. Certo. Antes de tudo, há uma função chamada build, que recebe o estado, que nada mais é do que o contexto. Vamos dar uma olhada rápida. Este é o nosso contexto: “Ticket 8812, cobrado duas vezes pelo mesmo pedido, sem resposta há 4 dias, cancele e devolva meu dinheiro, ou vou contestar a cobrança com meu banco.” Certo. Então, o estado, junto com as

26:40 instruções, sendo que cada instrução será uma pergunta. É assim que estamos formulando. Então, este loop for está tratando da primeira pergunta agora. Quando ele rodar de novo, haverá uma segunda pergunta. E aqui temos as opções. Mais uma vez, a resposta a esta pergunta é sim ou não. Também pode haver várias opções. Com essa configuração, vamos entrar na função build para verificar o que está acontecendo.

27:11 Agora, é bem simples. Pegamos as instruções, que correspondem à pergunta, e primeiro as codificamos. Pronto. Recebemos os IDs de token dessa pergunta. E fazemos isso de um jeito que colocamos primeiro um token de classificação, depois a pergunta e, em seguida, um token separador. Depois de fazer isso, vamos acompanhar esses separadores e delimitadores que estamos inserindo, para podermos extrair o que queremos extrair na busca pela

27:43 resposta. Então, aqui temos o trecho, o trecho da pergunta. E depois temos marcadores. Eles nada mais são do que as opções. Já sabemos que, para a primeira pergunta, as opções são não e depois sim. Então, estamos inserindo os pontos em que vou colocar meu token mask, para que eu possa acompanhá-lo durante a inference. Vocês verão tudo isso em instantes, caso ainda não esteja claro.

28:15 Certo. Os marcadores foram acrescentados aos IDs. Acho que, se acompanharmos os IDs, vai ficar claro. Os IDs serviam apenas para incluir a pergunta, mas notem que adicionamos o token de classificação e depois um token separador ao fim da pergunta. O token mask, que vamos observar e

28:47 inserir antes de cada opção, serve para acompanharmos as probabilidades dos tokens mascarados e fazermos algumas operações com elas, a fim de avaliar a força daquela opção, sua probabilidade ou a tendência para escolhê-la nessa pergunta. Certo. Quando rodarmos o loop de novo, espero que apareça mais um token mask, seguido da opção sim. Vamos ver isso acontecendo. Certo. Depois que isso for adicionado,

29:19 vocês verão que agora há outro token mask associado à opção sim. Todas as opções já foram percorridas. O loop não vai rodar mais. E, por fim, os IDs são montados e retornados. Então, se eu fizer uma verificação final, teremos o token de classificação, o separador, a pergunta, outro separador, o token mask da primeira opção, o segundo token mask da segunda opção, depois um separador e, por fim, todo o contexto, seguido novamente de um separador. Então, esses são

29:51 É bem jargão de encoder, paradigmas de treinamento no estilo BERT, MLM e modelagem de linguagem mascarada. Isso é usado o tempo todo na inference se você estiver usando BERT. Esse tipo de ordenação dos tokens nos permite diferenciar o que observar e o que não observar. Certo. Essa é a saída da nossa função de construção. E então voltamos, e esta é literalmente a chamada ao LLM. Passamos todos os nossos IDs de entrada. Agora,

30:22 o ID de entrada aqui, o meu novo, contém a pergunta, a opção, o estado, tudo. E o que estou fazendo é extrair o último estado oculto e depois transferi-lo para a CPU. Vamos dar uma olhada nisso em um segundo. A previsão do meu modelo terminou. É assim que ela se parece. Então, a forma do estado oculto é 54 por 768

30:52 por natureza. Então, literalmente, cada token tem seu próprio embedding, que obtivemos. Se você olhar o número de tokens que temos, eram 54. Portanto, o estado oculto retorna 54 embeddings, um para cada um desses tokens. Agora, se pedirmos o último estado oculto, o que ele vai nos dar, na verdade… vamos executar

31:24 tudo isso. Se eu executar isso, ele fará outra previsão do modelo. Então, o resultado é 1, 54, 768. Agora, se eu pedir o primeiro elemento, ele removerá a dimensão do lote e me dará apenas 54 por 768. Se eu verificar a forma disso, então é 54 por 768. E depois converto isso para float. E agora, na próxima etapa, o que estou

31:55 interessado em fazer é extrair apenas a consulta. Então, novamente, os intervalos e todas essas variáveis já foram criados. Escrevemos vários deles em código para economizar tempo. Vamos analisá-los agora. Se você olhar o intervalo, ele é uma tupla. O primeiro elemento do intervalo indica o token inicial ou a posição inicial da primeira pergunta. E o nove aqui representa a posição final

32:26 da primeira pergunta. Então, na verdade, literalmente, se eu fizer de um a nove e tentar decodificar, vou obter a própria pergunta. “O cliente está pedindo o dinheiro de volta?” Certo? Então, por que começamos em um aqui? Se eu tivesse começado em zero, teria encontrado o token de classificação, que quero evitar. A própria pergunta não contém esse token. Certo? Então, vou pegar

32:57 os estados ocultos das próprias perguntas. Se eu olhar, havia oito tokens na pergunta, então temos 768, e estou calculando a média deles. Estou tentando criar uma representação vetorial de 768 dimensões da própria consulta. É assim que você interpreta isso. Agora, se eu pegar isso e calcular a média,

33:27 você verá que vou obter 768 números. Vou repetir isso. Esta é a melhor representação da pergunta que temos do modelo. Porque estamos pegando todos os tokens da consulta, ou da pergunta, e calculando a média dos embeddings deles para obter a melhor representação da própria pergunta. >> [limpa a garganta] >> Certo. A partir daí, temos nossos marcadores, então sabemos com certeza que, na décima posição, novamente, podemos

33:59 verificar isso. Temos o token de máscara e, se olharmos o token anterior a ele, veremos que… ou melhor, se olharmos o token seguinte, veremos que ele é seguido pela opção que nos interessa. E agora, se eu olhar o marcador na décima segunda posição,

34:31 você verá que é o marcador associado à opção “sim”. Então, temos duas opções, uma máscara para cada uma delas. E então também pegamos e encontramos os estados ocultos delas. Novamente, obteremos essas duas representações em embedding das duas opções. E é aqui que

35:01 nossa implementação vai ser realmente a parte básica, e é aqui que há muita variação na implementação. Vamos tentar entender melhor. Temos a pergunta aqui. Depois, o token de máscara, a opção um, outro token de máscara, a opção dois, e assim por diante, até aparecer um separador e me fornecer todo o contexto, ou o

35:34 estado. E, por fim, fornece uma previsão. Então, o que estamos tentando fazer aqui é que cada um desses tokens gera um vetor de embedding de 768 dimensões. Agora, a pergunta: se eu pegar o intervalo da pergunta, a própria pergunta abrangerá alguns tokens, e calculo a média deles. Isso me dará uma representação limpa com 768 dimensões. Agora, esse token de máscara

36:06 também terá uma representação, então M1, e este também terá outra representação, assim. Então, você vê que as opções abrangem 2 por 768, e a consulta abrange 768 por 1. Esta é a opção. Esta é a consulta. Uma forma de pensar nisso é que posso fazer essas duas interagirem uma com a

36:36 outra. Posso literalmente calcular um produto escalar e ficar com dois escalares, e posso fazer algo com esses escalares, até mesmo aplicar um softmax simples a eles e considerá-los probabilidades. Essa é literalmente a forma mais simples de formular um problema de decisão. Não importa se você tem uma escolha, um valor nulo ou uma pontuação: você pega todos esses tokens e faz com que interajam uns com os outros. De qualquer forma, é uma atenção bidirecional, e então, a partir daí, você pega o vetor da consulta, cria uma representação dele, pega os vetores das opções mascaradas, faz com que interajam, e pode interpretar a saída como probabilidades.

37:07 Agora, obviamente, estamos usando um backbone pronto, sem nenhum treinamento. Isso não vai funcionar, mas serve de alicerce. O que mais você pode fazer com isso? Agora, você tem a opção um: uma camada linear.

37:41 Com isso, quero dizer que, em vez de pegar esses vetores de 768 dimensões de cada opção, posso passar cada um por uma camada linear e obter um escalar como resultado. Essa camada linear pode literalmente receber 768 valores e produzir um. Então, se um vetor de opção entrar, a probabilidade correspondente sai. Portanto, essa matriz de opções

38:13 aqui pode literalmente passar por uma camada linear e produzir dois escalares que representam as probabilidades correspondentes de cada opção. Em segundo lugar, posso fazer bastante fine-tuning. E posso definir um objetivo usando a forma como formulei o problema, em que a opção tentava interagir com a consulta, ou posso incluir as camadas lineares e depois fazer bastante fine-tuning. Na opção três, faço um

38:45 novo treinamento completo do backbone. Na prática, o que aconteceu é que os embeddings aprendidos — todas essas representações de 768 dimensões — são, na verdade, de qualidade e fidelidade bastante altas, mas o modelo não sabe que precisa produzir os resultados na forma de probabilidades. Então, qualquer número que você obtiver não fará sentido, mas todos os sinais já estão presentes. Portanto, isso depende muito da complexidade da sua tarefa e da quantidade de dados

39:17 que você tem, ou está disposto a usar e investir nisso. Você escolhe uma dessas opções. No estado mais básico, é possível obter probabilidades, mas isso não vai muito longe. Talvez seja necessário adicionar uma camada linear e fazer fine-tuning apenas dessa camada linear. Talvez seja necessário fazer bastante fine-tuning ou treinar novamente todo o backbone. Com isso em mente, voltamos aqui. Temos os vetores das opções. Lembre-se, eles têm a forma 2 por 768. E temos o vetor da consulta, que tem a forma 768 por 1. Então, fazemos com que interajam e os normalizamos.

39:47 Na verdade, você os passa pelo softmax e obtém probabilidades. Neste caso, se eu der uma olhada, você verá que saíram dois escalares. E eles podem ser interpretados como as probabilidades de responder àquela pergunta específica. Agora, sei que não são muito precisas. A resposta deveria ser “sim”, enquanto o resultado está indicando apenas 30%, mas lembre-se de que os sinais dos embeddings são de altíssima qualidade. É preciso se acostumar um pouco com essa interação. Leva um pouco de

40:19 um fine-tuning leve, que é onde entram nossas três opções. Certo? Então, com isso, acho que já concluímos nossa representação do encoder de como o Jeff funciona. Espero que isso tenha ajudado você a desenvolver uma intuição sobre tudo o que é possível fazer e tudo o que é necessário fazer para que isso funcione. Espero que ajude. Certo. Vamos conferir o que acontece no mundo dos decoders. Acontece que também é possível usar um decoder para formular o

40:50 problema de um jeito que exija apenas uma passagem forward, somente a etapa de pré-preenchimento, sem etapa de decodificação, para alcançar todos os objetivos do Jeff. E é assim que funciona. Vamos empacotar todo o estado de antemão. Isso é exatamente o oposto do que fizemos com encoders, porque desta vez ele pode ser armazenado em cache. Então, se houver um estado — e, como esta é a máscara causal, se o estado tiver um KV cache existente —

41:21 posso simplesmente continuar trocando as perguntas aqui e reutilizar o KV cache também. Outra coisa que posso fazer é, se tiver um comprimento de contexto grande o suficiente, empacotar aqui quantas perguntas e opções de resposta couberem. Então, imagine que você tem um contexto de 32K e que o estado consome apenas 1K. Agora, imagine que você tem 31K de contexto para

41:51 preencher com o máximo possível de perguntas e opções. É meio evidente que o Jeff adotou uma abordagem de decoder, porque ela oferece suporte a até 5.000 perguntas sobre um único estado. Então, meu melhor palpite é que talvez estejam fazendo decodificação, mas, novamente, eles não divulgaram nada, então não dá para ter certeza. O primeiro token, ou melhor, a abordagem que vamos adotar aqui, é formular o problema desta maneira: aqui está o contexto,

42:22 aqui está a pergunta, e vamos deixá-la terminar com dois-pontos. Isso configura o LLM quase como uma tarefa de preenchimento automático. E, assim que ele gerar um token, vamos examiná-lo. Esse token obviamente terá uma distribuição de probabilidade completa sobre o tamanho do vocabulário. Na verdade, vamos nos concentrar apenas nos tokens que estão presentes nas nossas opções,

42:54 pré-treinadas — ou melhor, predefinidas. Então, se uma escolha tiver apenas “sim” e “não”, vamos olhar apenas as probabilidades dos tokens “sim” e “não” nessa primeira etapa de decodificação de token. Vamos ignorar todos os outros e tratá-los como probabilidades para retorná-las. Em vez disso, você também pode formular o problema desta maneira, em que reformula o problema quase como uma questão de múltipla escolha. Você tem

43:26 as opções A, B, C e D, e então dá um prompt ao modelo de linguagem para responder qual é a resposta e nada mais. A resposta deve ser A, B, C ou D. Com base na letra que ele escolher, você retorna a probabilidade correspondente. Essas duas abordagens, porém, não exigem treinamento. Todo o resto exige. Então, vamos ficar com esta parte, e é possível extrapolar isso razoavelmente porque, novamente, é só pedir ao modelo que gere

43:58 um caractere, que é uma das opções disponíveis. E, novamente, você pode ter uma cabeça de ponteiro de decisão para a opção, que passa por uma camada linear para produzir uma distribuição de probabilidade interpretável. Foi isso que o Jeff implementou. Esta é uma alternativa open source ao Jeff. Depois, a Cloudflare criou o Clef, que é basicamente uma implementação com span agrupado de toda a opção, junto com uma cabeça linear específica.

44:30 Então, ela leva em conta a opção inteira para essas perguntas e, assim, nunca pode realmente errar em caso de duplicatas. Só para deixar claro o que eu quis dizer com essa última parte: isso pode ser, digamos, uma pergunta. Acabei de inventar. A opção um é “fraude” e a opção dois pode ser “análise de fraude”.

45:03 Certo? Então, se você tiver uma tarefa perfeitamente formulada no estilo do Jeff, com a opção um e a opção dois, note que o primeiro token é exatamente igual. É aí que nossa implementação simples vai falhar. Ela não vai saber a diferença entre as duas, porque os primeiros tokens são literalmente iguais, e provavelmente vai retornar 50/50. Na minha implementação, tratei isso de modo que, se houver tokens duplicados ou tokens iguais, vamos gerar um erro pedindo ao usuário que altere a forma como formulou a

45:34 opção. Então, ela pode ser simplesmente “análise”, em vez de “análise de fraude”. E aí esse problema desaparece, certo? Novamente, Letta não tem esse problema. Se você simplesmente chamar as opções de A e B e pedir ao LLM que retorne uma dessas letras, acho que deve funcionar. Mas o span agrupado, em particular, leva isso ao extremo e evita completamente esse problema. Mesmo que as opções sejam totalmente duplicadas, esta implementação específica consegue desambiguá-las.

46:05 Não vamos examinar nenhuma dessas três implementações. Vamos nos limitar à tarefa do primeiro token e nos concentrar principalmente em como o cache pode ser reutilizado e como ocorre a troca de perguntas. Isso, por si só, já vai ser um desafio. Antes de entrarmos no código, quero que você entenda como vamos formular este bloco de máscara causal, porque isso vai ser muito importante para visualizar e entender como podemos empacotar várias

46:35 perguntas em uma única sequência. Então, digamos que temos uma pergunta. Temos algum contexto. Meu nome é Vivek. A partir daí, a pergunta fica: “Ah, qual é o meu nome?”. E depois talvez: “Qual é

47:07 ou, melhor, formulamos assim. A opção pode ser “Vivek” ou alguma outra coisa. E “Qual é meu sobrenome?”. E a resposta pode ser qualquer coisa. Então, opção A e opção B. Certo. Vamos formular o problema desta maneira. Afinal, é uma máscara causal.

47:38 Então, token um, token dois e assim por diante. Se você visualizar a matriz de atenção aqui, assim, o primeiro token só pode prestar atenção a si mesmo e não pode prestar atenção a mais nada. Agora, o segundo token pode prestar atenção a si mesmo e ao token anterior, e assim por diante. Isso continua, certo? Então, em cada etapa, cada token só pode olhar para trás, não para a frente.

48:09 Agora, isso fica um pouco complicado quando você tem várias perguntas e quer empacotá-las todas em um determinado contexto. Certo. Digamos que a pergunta tenha três tokens e, para fins deste exemplo — ou melhor, que o estado tenha três tokens. Para simplificar, você pode fazer uma pergunta junto com as opções nos dois tokens seguintes.

48:40 E a pergunta dois também ocupa dois tokens. Novamente, fazemos isso para poder ver a matriz completa. Então, teremos estado um, estado dois, estado três e, em seguida, Q1, Q11, Q12 e depois Q21, Q22. E a mesma coisa acontece aqui: S1, S2,

49:11 S3, Q11, Q12, Q21 e Q22. A maneira como queremos empacotar isso é: aqui teremos a atenção convencional, como você já conhece, a atenção mascarada típica. Máscara causal, certo? Agora, esta pergunta deve conseguir acessar todo o contexto. Então, não há problema em continuar a máscara causal que estamos construindo aqui, certo?

49:45 Até aqui, tudo certo. Agora, o que deve acontecer com a pergunta dois? Idealmente, a pergunta dois deve ter acesso para consultar todo o estado, mas por que ela deveria ter acesso a qualquer conteúdo da pergunta um? Quer dizer, em teoria, é possível expor a pergunta dois ao contexto da pergunta um, mas pense no que você está fazendo aqui. Na verdade, está contaminando o contexto com uma pergunta que você nem deveria conhecer ou consultar.

50:16 Isso fará com que, se você fizer as perguntas em uma determinada ordem, obtenha respostas diferentes. Portanto, os resultados não serão muito reproduzíveis no seu domínio. Curiosamente, pelo que estudei do Clef da Cloudflare, eles implementaram isso sem máscara, e isso simplesmente não faz sentido para mim. Porque, quando você altera a ordem das perguntas, obtém resultados diferentes.

50:46 outputs, tenho certeza. Então, se você olhar para isto, a pergunta um — ou melhor, a pergunta dois — deve ter acesso a todo o estado anterior, não deve ter acesso à primeira pergunta e, claro, deve ter acesso ao próprio token. Então, ela tem acesso ao estado, não tem acesso à pergunta e tem acesso a si mesma. E é assim que

51:17 vamos ter que fazer. Então, o estado será o próprio bloco e, quando chegarmos à pergunta, ela terá acesso ao próprio bloco e ao estado. A próxima pergunta terá acesso ao próprio bloco e ao estado. E isto ficará completamente mascarado. Construir essa máscara vai exigir algumas manobras com arrays, que veremos em breve. E é por isso que eu queria realmente me concentrar nos

51:48 princípios fundamentais e nos aspectos centrais da construção disso, em vez de analisar um conjunto variado de implementações e nos confundir. Quando você entender esse fundamento, acho que as diferentes variedades de implementação e os diferentes ajustes nas abordagens adotadas pelos vários provedores ficarão facilmente ao seu alcance. Sem mais delongas, vamos direto à implementação. Então, temos aqui o script de decodificação puro.

52:19 E, como já temos alguma perspectiva sobre a teoria, vamos dar uma olhada rápida no estado. Este é o contexto que será fornecido. Então: “Só quero meu dinheiro de volta em dobro.” E as perguntas são basicamente duas. Se eu olhar para a primeira: o cliente está pedindo o dinheiro de volta, sim ou não? E qual equipe é responsável por isso? A equipe de cobrança ou a equipe técnica? Essas são as duas perguntas que queremos fazer. Isso também pode ser facilmente extrapolado para uma pergunta de pontuação.

52:49 Incluí apenas null e opções neste caso. Certo. Então, antes de tudo, carregamos nosso modelo. Este é um modelo somente decodificador, então usaremos exatamente o mesmo modelo que usamos na etapa de decodificação vanilla. Este modelo será o Qwen de 3,6 bilhões de parâmetros. E, a partir daí, fornecemos o token, o modelo e o estado para podermos recuperar nosso cache. Essa é uma das propriedades mais desejáveis dos modelos

53:19 somente decodificador. Posso pré-calcular o cache do estado e, depois, continuar respondendo a perguntas. Posso agrupar essas perguntas ou simplesmente fazer um loop e respondê-las uma por uma. Certo. Vamos entrar um pouco na etapa de prefill para ver o que está acontecendo. Assim, você poderá ver que todo o estado está sendo tokenizado deste modo. E então, na verdade, estou executando uma inference do modelo aqui. E a saída inclui os valores de chave anteriores, para

53:50 que eu possa obter meu KV cache para este estado específico. Então, novamente, se eu olhar para os IDs de entrada, são 38 tokens. Pelo que entendo, normalmente o KV cache terá algumas camadas e algumas cabeças de KV. Então, tudo isso junto e, claro, os valores de chave — haverá dois. Portanto, dois vezes o número de camadas, vezes o número de cabeças, vezes a dimensionalidade do

54:21 embedding. Tudo isso será importante na hora de extrair o KV cache. Então, quanto à dimensionalidade, se eu olhar… Opa. Deixe-me verificar o comprimento. Há 28 camadas neste modelo. Isso nos diz alguma coisa. Certo. E cada camada tem este KV cache.

54:52 Ou melhor, como acessamos o primeiro valor? Acho que fazemos isso pela propriedade layers. Esta é a primeira camada. E aqui posso procurar aquele… Então, pronto. Temos as chaves e os valores como tensores aqui. Se eu olhar para esta forma, terei o tamanho do lote. Então,

55:23 isso tem oito cabeças de KV, 38 tokens e 128 dimensões. E há um total de 28 camadas. Então, isso vezes 28 vezes dois, porque aqui temos apenas as chaves; também temos os valores. Certo. Então, a partir daqui, pré-calculamos o cache do estado, por assim dizer, e agora estamos prontos para responder às perguntas. Então, vamos… Deixe-me executar até aqui e, em seguida,

55:53 vamos nos aprofundar nesta função específica, answers. Certo, então já lidamos com o estado ou o contexto. Agora, vamos lidar com as perguntas. Temos as perguntas aqui. É uma lista de tuplas. Cada tupla contém uma pergunta e uma lista de respostas — ou melhor, uma lista de opções. Certo.

56:24 É aqui que começamos todo o exercício de tentar garantir que os tokens certos sejam atendidos pelos tokens certos. Todo esse processo de impedir que a pergunta dois consiga ver a pergunta um, e assim por diante. Então, estamos construindo esta máscara de perguntas. Vamos dar uma olhada rápida. Temos a largura dos segmentos. Isso conterá os limites das próprias perguntas. Então, se você observar…

56:56 se eu tiver um total de 35 tokens que compõem as perguntas. O estado já foi armazenado em cache. Não precisamos nos preocupar com ele. Estes são os 35 tokens que contêm as duas perguntas e suas opções. E vimos que aproximadamente metade deles é da pergunta um e a outra metade é da pergunta dois. Então, os segmentos indicam os índices reais das perguntas.

57:26 Certo. Vamos convertê-los em um tensor do torch. Agora, construímos uma máscara causal básica com o A range e, em seguida, fazemos com que seja maior ou igual à própria transposta, para obter nossa máscara causal. Se dermos uma olhada rápida, a máscara causal é simplesmente assim: o primeiro elemento é true, os dois primeiros elementos da segunda linha são true, os três primeiros elementos da terceira linha são true, e assim por diante. Esta é a máscara causal vanilla.

57:59 Vamos precisar fazer mais coisas, como veremos, para obter aquela abstração de pergunta para pergunta que queremos alcançar. Se os tokens pertencem ou não à mesma pergunta é determinado pelos segmentos, e temos os segmentos em um tensor do torch chamado seg. Então, vamos fazer a mesma coisa, mas com uma cláusula de igualdade em relação à transposta. Assim, poderei determinar se dois tokens fazem parte da

58:29 mesma pergunta ou não. Se eu percorrer o tensor da mesma pergunta que tenho, vamos ver o que ele produz. Você verá que os primeiros tokens aqui são todos true entre si, e o último conjunto de tokens aqui também é todo true entre si. Portanto, há uma segmentação clara: estes são os tokens da pergunta um, e estes são os tokens da pergunta dois. Agora, se eu pegar isso e fizer interagir com

59:00 a máscara causal que construímos nas etapas anteriores, surgirão algumas propriedades interessantes. Vamos esperar isso acontecer. Certo? Na própria próxima etapa, visible deve ser a concatenação de torch.ones com a largura do comprimento do estado, junto com a interação entre same question e a máscara causal que construímos. Isso nos dará o estado desejado que queríamos alcançar. E acho que isso percorre as

59:30 perguntas em um loop. Certo, temos o tensor visible conosco. Vamos examiná-lo mais a fundo. Tínhamos a máscara causal, tínhamos a máscara same question e fizemos as duas interagirem para chegar a isto. Certo. Podemos ver que esta forma inteira é 35 por 73. Portanto, há 73 tokens ao todo, que constituem o estado e todas as

1:00:03 perguntas. Elas foram agrupadas. Como os primeiros tokens já foram armazenados em cache, só preciso me preocupar com os 35 tokens que compõem a pergunta. Se eu olhar aqui, tenho esta parte inteira do quebra-cabeça. Tenho todas as perguntas comigo. Tenho esta parte. Por isso, são 35 por 73. Não tenho a parte de cima porque já a armazenei em cache. Então, esta parte já está

1:00:34 armazenado em cache. Só estou interessado em toda esta parte. E, por isso, tenho esta parte com Hope que está clara. Então, no momento em que você vir «visible», verá que eles se parecem com o estado. A primeira parte é o estado em que tudo é verdadeiro. De novo, se você olhar para esta parte, tudo nela é «true» denso. E então a única parte interessante da decodificação

1:01:06 começa quando ela passa do número de estados para este lado. Então, se eu fosse realmente fatiar e dividir isso para ignorar talvez os primeiros 38 tensores, que contêm o contexto. Vamos fazer isso. A partir do 38º token, você já verá que temos esta máscara causal em execução, porque esgotamos o estado que continha 38 tokens. E

1:01:39 então, a partir daí, você verá que tudo no canto inferior esquerdo é falso, porque esses são os tokens da primeira pergunta, e agora estamos cuidando da segunda pergunta. E então tudo é causal apenas para a pergunta dois. Tudo é causal para a pergunta um aqui. Todo o resto é falso no canto superior direito. Todo o resto é falso no canto inferior esquerdo. Então, estamos tentando manter essa máscara que vimos aqui. Essa máscara organizada em que tudo aqui precisa ficar ativado. Tudo aqui precisa ficar desativado. E tudo aqui também precisa

1:02:10 ficar desativado, e assim por diante, certo? Então, só olhamos para as perguntas considerando o estado delas. É isso que queremos alcançar. Espero que tenha ficado claro como estamos construindo essa máscara. Ela é um pouco diferente da máscara causal de atenção padrão. Por isso, foi preciso analisá-la em profundidade. Mas, assim que recuperamos a máscara, todo o resto permanece igual. Vamos percorrer o mesmo processo que percorremos em um estado de decodificação padrão, no qual fazemos apenas uma

1:02:43 previsão, e somente uma previsão. Então, temos todas essas segmentações e tudo mais. Vamos passar a máscara de atenção que construímos junto com os IDs que contêm todo o estado, a pergunta e assim por diante. Vamos obter… vamos passar o nosso cache KV aqui, porque já fizemos isso para a pergunta. Também vamos passar os IDs de posição. E

1:03:13 se você olhar aqui, verá que ele vai de 38 a 53. Então, lembre-se de que, embora estejamos fornecendo um tensor novo, um cache de 38 tokens já foi fornecido. Portanto, as posições corretas precisavam ser preenchidas para que ele pudesse calcular adequadamente os embeddings posicionais, o RoPE e tudo mais. Certo? Então, obtemos os logits.

1:03:48 E vamos executar até aqui. Então, obtemos os logits para isso. E, se você olhar para o formato dos logits, normalmente um LLM deve fazer o seguinte: para o número de tokens que você passou, ele devolverá exatamente a mesma quantidade, mas, para cada posição de token, haverá um tamanho de vocabulário que conterá a distribuição das probabilidades em cada posição. Agora, só nos interessaremos pelo último token, porque estamos supondo que o

1:04:19 próprio último token contém nossa resposta sem duplicatas. Então, vamos descer até aqui. Então, para cada conjunto de IDs de opções de decisão das instruções em questions, decisions e candidates, vamos dar uma olhada rápida no que esses arrays fazem. questions é simplesmente o texto das perguntas. Você já viu isso antes. decisions, 18 e 34, são na verdade o

1:04:51 ponto de decisão final de cada pergunta. Então, se eu olhar para o primeiro 18, você verá que ele contém a pergunta junto com as opções. E, se olhar para todo o resto a partir dali,

1:05:23 verá que também temos a segunda pergunta, bem como suas opções. Portanto, as decisões são tomadas com base nessas perguntas e em suas opções. E, claro, temos os candidates. Estes são os IDs de token para «yes» ou «no», e «billing» e «technical». Então, vamos dar uma olhada rápida nos candidates aqui também, para podermos atribuir probabilidades. Este é «yes», este é «no», este é «yes». Então, 902 é «no», 9834 é «yes», 30531

1:05:55 é «billing» e 10916 é «technical». Então, é assim que isso está se configurando. Vamos enumerar este zip de questions, decisions e candidates. Agora, obviamente, só nos importam os pontos de decisão. Então, vamos direto a eles. É aqui que termina a pergunta um, e ela conseguiu considerar todo o estado anterior. E é aqui que, no próximo loop, termina a pergunta dois, que também terá considerado todo o estado e

1:06:27 o contexto anterior. Então, esse é o último token que me interessa; vou pegar seus logits e convertê-los em probabilidades. No meu caso, eles representarão as probabilidades correspondentes às próprias opções. Vamos dar uma olhada. Temos as probs aqui. Uma olhada rápida. Para a primeira pergunta, temos 95% de confiança de que

1:06:57 o cliente está pedindo o dinheiro de volta. Então, veja: com apenas a decodificação, funciona de imediato. Diferentemente de embedding, que precisa de algum tipo de fine-tuning. Certo. Para a segunda pergunta, vamos percorrer o mesmo processo: qual equipe é responsável por isso? É billing ou technical? E então veremos aqui as probabilidades dessas duas opções. Se houver três opções, aparecem três probabilidades. Isso não é um problema. Temos 97% de confiança de que é da equipe de

1:07:29 billing. E cerca de 2,5% para o restante, o que significa que há uma probabilidade de que seja da equipe técnica, certo? Então, seguimos daqui até o fim. E, na prática, quando chegamos a esta etapa, você verá que tenho outra linha de código aqui que diz: «Resposta: quão irritado está o cliente? Calmo ou furioso». Essa é uma pergunta no estilo de pontuação que você pode executar

1:08:00 por conta própria. Não me dei ao trabalho de executá-la. Mas o que eu queria mostrar sobre o estilo somente decodificador é que ele não exige recalcular o contexto ou o estado. Eles foram pré-calculados. Então, você pode continuar adicionando mais perguntas aqui e alterá-las, mudar as opções; nada disso importa, nada disso vai mudar. Vai continuar tudo igual. Então, o estado nunca mudará. Ele foi armazenado em cache. As perguntas

1:08:32 você pode, na verdade, fazer também como acompanhamento com o usuário, em vez de agrupá-las. Espero que isso dê a você uma boa compreensão de como funciona a inferência JAF somente com decodificador. Como bônus, vamos tentar abordar também como funciona o treinamento. Não vamos passar pelo código, nem por nenhum dos conjuntos de dados e coisas assim. Mas vamos formular o objetivo de treinamento. Vamos examinar a função de perda e como ela é calculada, além de

1:09:03 algumas das variedades existentes, e você terá uma noção de RLCD na teoria. Então, antes de entrarmos na parte de treinamento, eu queria falar sobre algumas limitações do método que mostrei e sobre como é possível contorná-las facilmente. Quando duas respostas começam da mesma forma, como já mostrei antes, se duas respostas começarem com o mesmo token, nossa implementação vai falhar. Agora

1:09:34 uma coisa simples que você pode fazer para adaptar isso à abordagem padrão simples que adotamos é simplesmente renomear as opções. Caso contrário, precisamos pontuar cada token, e isso vai exigir uma passagem extra. Então, em vez de apenas uma etapa de decodificação, talvez precisemos passar por várias etapas de decodificação e depois usar uma estratégia de pontuação por média agregada. Ou você pode realmente

1:10:04 condensar cada opção e codificá-las com letras, como A, B, C e D, exatamente como as perguntas de múltipla escolha são formuladas e respondidas. Isso também elimina a colisão. E, claro, você já viu isso antes com encoders: o token de máscara é colocado logo antes de uma opção. Como a natureza é posicional, isso nunca confunde um encoder. Tokens duplicados nas opções são um problema apenas para decodificadores, não para encoders

1:10:34 problema. Sim. Então, com isso, vamos chegar à parte do treinamento. Vamos passar por exemplos de como o treinamento funciona, mas não vamos mostrar uma execução de treinamento nem produzir um modelo de fato. Vou deixar isso como exercício. Alguns dos scripts extras, ou scripts bônus, que vou compartilhar como parte do GitHub vão incluir essas etapas de treinamento, mas também vão usar dados sintéticos. Portanto, terão pouquíssima utilidade no mundo real.

1:11:07 Mas tenho certeza de que vocês podem usá-los como modelo e reconstruí-los para o caso de uso de vocês. Então, com isso, vamos analisar uma etapa de treinamento já resolvida. A forma como esses treinamentos são formulados é por destilação de professor e aluno. É basicamente isso. Ou você tem dados pré-rotulados, ou tem um modelo professor capaz de fornecer algum tipo de probabilidades. Então, digamos

1:11:38 que, para a pergunta anterior — isto é um problema de cobrança, um problema técnico, um problema de conta ou outra coisa? —, você tem duas opções, três opções, e o modelo professor diz 0,7, 0,2 e 0,1. Essa é a distribuição de probabilidade. Agora, pense nos nossos scores como logits ou escalares que saem da camada linear ou algo do tipo. Portanto, eles ainda não passaram pela softmax. Esses são os scores. Os scores brutos são +1, +0,8 e +0,20.

1:12:12 Agora, nossas respostas, nossas probabilidades, são 0,44, 0,36 e 0,19. Repare que elas mantêm a ordem de classificação, mas, ainda assim, eu gostaria que nossa resposta fosse mais próxima da do professor. Então, o que fazemos agora? Usamos o paradigma chamado GRPO, otimização de política relativa ao grupo. É um conceito de aprendizado por reforço.

1:12:43 Se vocês ainda não viram GRPO, provavelmente deveriam desenvolver uma intuição sobre ele. Acho que tentar explicar o que o GRPO faz desviaria muito o nosso foco, mas, em essência, ele tenta seguir várias trajetórias e calcular a média delas. Se uma trajetória segue um bom caminho ou gera uma resposta de alta fidelidade, subtraímos a média dela, e o que sobra é algo chamado vantagem. Usamos essa vantagem para construir uma função de perda e, então, retropropagamos para

1:13:13 garantir que o LLM produza respostas boas com mais frequência do que as ruins. Isso é o aprendizado por reforço em poucas palavras. Neste caso, não temos várias trajetórias porque, de novo, restringimos o nosso modelo a gerar apenas certas probabilidades de token. Então, vamos ter que criá-las do nada. O que fazemos é pegar esses scores. Temos estes scores aqui. Então, temos 1, 0,8 e 0,2, e adicionamos perturbações. Adicionamos perturbações aleatórias a esses scores.

1:13:45 Digamos que a trajetória um, com um certo tipo de perturbação aleatória, produza estes scores. No segundo passo, adicionamos outro conjunto de perturbações aleatórias, e ela faz isto, isto e isto. Então, em vez dos scores originais, nós os transformamos e adicionamos perturbações aleatórias para chegar a estes scores específicos. Agora, se os convertermos em probabilidades, eles ficam mais ou menos assim. Agora,

1:14:17 a primeira trajetória é uma que me agrada, porque se aproximou da direção do professor, mantendo a ordem de classificação. Então, eu daria a ela uma nota maior do que às outras. E, como cada uma dessas coisas recebe uma nota, a média delas será subtraída, e vocês verão que esta tem uma vantagem positiva. É exatamente isso que eu estava tentando explicar: a vantagem. Portanto, qualquer coisa que tenha uma vantagem positiva terá uma perda menor e será mais favorecida

1:14:47 do que as outras. Isso vai orientar o nosso modelo aluno na direção certa, aproximando-o de imitar a distribuição do professor, por assim dizer. Certo? Só para mostrar alguns dos cálculos: estes são os valores de recompensa que obtivemos: -0,33, -0,52, -0,5241 e -0,4154. A média deles é

1:15:18 -0,4498. Agora, se eu pegar a recompensa e subtrair a média dela, estou essencialmente fazendo (-0,3380 - (-0,4498)) dividido por 0,0901, que é, mais uma vez, o desvio padrão de toda essa distribuição. E o resultado é o que estou chamando literalmente de vantagem. Então, estas duas trajetórias têm uma vantagem positiva, enquanto estas duas têm uma vantagem negativa. E, se vocês olharem para as respostas em si, vão entender intuitivamente por que elas têm vantagem positiva. Tínhamos 0,44, que é a nossa linha de base. Isso aumentou um pouquinho, enquanto este valor diminuía e este diminuía ainda mais; ou talvez este tenha aumentado um pouco, o que… Então, há uma vantagem geral

1:15:48 em se aproximar desta trajetória. Agora, esta mantém muito bem a ordem de classificação e também se aproxima mais do modelo professor; por isso, tem uma vantagem maior. Então, o mecanismo de pontuação foi elaborado dessa forma: com essas trajetórias, as respostas melhores têm mais vantagem, o que, por sua vez, afeta mais a perda. Certo? A última pergunta que ainda resta aqui é: se vocês olharem para as respostas em si, vão entender intuitivamente por que elas têm vantagem positiva. Tínhamos 0,44, que é a nossa linha de base. Isso aumentou um pouquinho, enquanto este valor diminuía e este diminuía ainda mais; ou talvez este tenha aumentado um pouco, o que… Então, há uma vantagem geral de

1:16:19 se aproximar desta trajetória. Agora, esta mantém muito bem a ordem de classificação e também se aproxima mais do modelo professor; por isso, tem uma vantagem maior. Então, o mecanismo de pontuação foi elaborado dessa forma: com essas trajetórias, as respostas melhores têm mais vantagem, o que, por sua vez, afeta mais a perda. Certo? Então, de onde esses scores apareceram magicamente? Vamos analisar um deles para ver como obtemos ou

1:16:51 calculamos um desses scores. Este score, -0,3380, apenas da primeira trajetória, é uma combinação de vários scores. Temos o score logarítmico. Temos o score esférico. É uma combinação dos dois. O score logarítmico tenta responder à pergunta: “Você atribuiu probabilidade às opções às quais o professor atribuiu probabilidade?” O score esférico pergunta se a resposta inteira

1:17:21 aponta na mesma direção que a do professor. Portanto, no geral, é preciso haver uma trajetória de aproximação à resposta correta, mas também é preciso que, se um modelo professor atribuir uma pontuação alta a uma opção específica, o modelo aluno também atribua a ela uma pontuação igualmente alta. Por um lado, ele imita a distribuição; por outro, mantém a consistência direcional. Essa é a intuição. A forma de implementar isso é a seguinte: temos aqui as probabilidades do professor, 0,7, 0,2 e 0,1. Temos as probabilidades do aluno

1:17:53 aqui: 0,52, 0,31 e 0,16. Calculo o logaritmo disso, que resulta nisto. Depois, pondero esses logaritmos pelas probabilidades do professor. Assim, quanto maior a probabilidade atribuída a uma opção específica, maior será o peso dela na soma total. Agora, quando tenho essa soma, vou colocá-la como um termo específico e somá-la ao score esférico. Como funciona o score esférico? Mais uma vez, são simplesmente

1:18:25 probabilidades ponderadas pelas probabilidades do professor. Portanto, não estamos apenas calculando o logaritmo das probabilidades ponderadas pelas probabilidades do professor; também estamos literalmente usando as probabilidades ponderadas pelas probabilidades do professor. Estamos pegando 70% da primeira probabilidade do aluno, e assim por diante. Normalizamos tudo isso. Então, o score final fica assim, e é ele que estamos

1:18:56 inserindo aqui. Esses dois, juntos, constituem a recompensa de uma trajetória específica. Agora, é aqui que entram as nuances. 99% disso é muito claro. Há um modelo professor que fornece as probabilidades. Há um modelo aluno que está tentando segui-las e imitá-las. Esses são o score logarítmico e o score esférico.

1:19:27 Mas também há um último ajuste, aplicável somente a uma interface específica. Lembrem-se: Jev tem as interfaces de score, nula e de escolha. Apenas para as interfaces de score, temos outra implementação chamada RPS, ou score de probabilidade de classificação. O que estamos tentando responder aqui é que o score terá níveis ou será de natureza ordinal. Então, digamos que eu tenha uma pergunta em que

1:19:59 As respostas podem ser frustradas ou muito irritadas. Agora, isso é um espectro. Vai de… Você pode visualizar como algo que vai da esquerda para a direita. Então, digamos que a verdade aqui indique que o cliente está muito irritado. Assim, é zero para calmo, zero para frustrado e um para muito irritado. Agora, se eu disser “frustrado” em vez de “calmo”, você vai notar que dizer “estou frustrado” é um pouco melhor.

1:20:33 Quer dizer, eu diria que o modelo está um pouco mais bem calibrado do que outro modelo que diz “calmo”. Certo? E isso só se aplica a previsões ordinais. Então, neste caso, se eu realmente prever isso: 0,1, 0,8, 0,1, então a pontuação logarítmica será… em comparação com 0,8, 0,1, 0,1. Portanto, se eu simplesmente inverter isso, a pontuação logarítmica vai me dar exatamente a mesma

1:21:04 pontuação. Mas você vê que o RPS será diferente. A pontuação de probabilidade, a pontuação de probabilidade acumulada, será diferente. É a diferença quadrática entre os totais acumulados, calculada em média ao longo de todos, menos um. Então, em cada etapa, vou calcular esse RPS apenas para as perguntas de escolha e incorporar isso à minha função de perda de alguma forma.

1:21:37 Certo? Então, a função de perda é, por fim, igual à pontuação logarítmica, a 75% da pontuação esférica e, basicamente, você pode atribuir o peso que quiser ao RPS negativo. Então, essa é a avaliação e, para construir a função de perda final, eu… na verdade, já vi coisas como entropia cruzada mais essa perda de RL. Então, vamos usar entropia cruzada completa mais a perda de RL

1:22:09 e, por perda de RL, quero dizer que ela vai conter a pontuação esférica mais a pontuação logarítmica. E, se for um estilo de pontuação, também terá o RPS incorporado. Então, essa é a proposta central de como o RL CD funciona. Há um componente completo de entropia cruzada e, depois, um componente completo de RL.

1:22:41 Para isso, as trajetórias são geradas com a ajuda de GRPOs, adicionando pequenas perturbações aleatórias, e ele tenta combinar duas funções de perda para chegar à perda final de RL. Por fim, você faz a retropropagação disso em tudo o que estiver treinando. Você pode optar por treinar uma camada linear simples, ou uma LoRA, ou também fazer fine-tuning completo da rede principal. A escolha é sua, depende do que você está disposto a investir e da métrica de precisão

1:23:13 que você quer para o seu caso de uso, e assim por diante. Depende mesmo. Uma observação rápida sobre as implementações, algumas comparações entre elas, vantagens, desvantagens e coisas do tipo. Implementamos os dois paradigmas para Jev: o baseado em codificador e o baseado em decodificador. Algumas dimensões que você deve avaliar para decidir se adota a abordagem de codificador ou de decodificador são as seguintes. Se o estado precisa ser mantido entre as perguntas, o codificador precisa relê-lo toda vez, mas o decodificador

1:23:45 pode lê-lo uma vez e armazená-lo em cache. Então, se você tem muitas perguntas sobre um conjunto finito de contexto, acho que o decodificador é a abordagem certa. Agora, o custo das perguntas se acumula: ele cresce a cada pergunta. Isso permanece constante; só as perguntas são novas. Se você quiser mais comprimento de contexto, se tiver documentos mais longos para processar, então o decodificador é a abordagem certa. O estado é lido com a pergunta em vista. De novo, como esta é uma implementação bidirecional, como as que os codificadores fazem, ela

1:24:17 sempre terá uma pequena vantagem em relação à máscara causal ou à atenção causal. As opções são vários tokens. Como o marcador fica na frente, é muito fácil inserir tokens de máscara antes dessas opções e fazer com que funcionem bem em escala. De novo, com o decodificador, haverá uma pequena desvantagem em comparação com os codificadores. Para vários rótulos de uma vez, podemos facilmente substituir a camada de saída por uma sigmoide. Ficamos

1:24:47 limitados à softmax, então vamos precisar fazer um pouco de engenharia de modelos, ajustar um pouco a arquitetura para que isso aconteça. Agora, normalmente, os codificadores com menos parâmetros fazem um trabalho melhor do que seus equivalentes. Então, se eu tiver um modelo codificador com 400 milhões de parâmetros, vou precisar de um modelo decodificador mais capaz para fazer o mesmo trabalho. Esse é o aspecto central. Então, se você estiver realmente com pouca

1:25:18 memória ou obcecado por velocidade, precisa levar todas essas dimensões em consideração, junto com o seu domínio e a definição do problema, para fazer isso funcionar. E, por fim, o que você precisa escrever é uma sequência para a pergunta, em comparação com as posições de máscara e o cache. Essas são as compensações de engenharia que você vai ter de levar em conta. Certo. Acho que isso resume tudo o que tentamos reunir aqui. Espero que tenha gostado e que se divirta aprendendo. Muito obrigado

1:25:49 pelo seu tempo.

Transcrição original

[00:00] Learn how to build and master Jev-style decision models from scratch using PyTorch and Hugging Face backbones. By replacing token-by-token auto-regressive decoding with a single forward pre-fill pass, Jev bypasses text generation entirely to deliver bounded low-latency typed decisions, such as choices, ordinal scores, and boolean nulls. You will learn about inference paradigms and explore training workflows

[00:30] with RLCD to produce honest, well-calibrated probabilities for real-world software logic. >> Hello and welcome to uh learning Jev from scratch using only PyTorch and bare-bones libraries like Hugging Face. We will use pre-trained models to uh leverage those as a backbone, but we’ll try to learn the first principles of inference and some of the training from scratch on how Jev works, so that you have an idea on uh

[01:01] this particular paradigm of how LLMs work. So, we’ll cover Jev and RLCD, reinforcement learning for calibrated decisions, in this video. Hope you enjoy. So, the first thing we want to cover is uh what is Jev, what is RLCD. We’ll probably get to RLCD towards the uh end of this uh particular series, because I really want to cover inference first and the different paradigms under which you can do inference, and then work myself backwards for uh

[01:33] understanding the training workflows. So, uh Jev in its essence is very simple. If you understand how an LLM works, an LLM has two stages, uh pre-fill and decode. Uh the pre-fill is the stage where it eats up all of the prompt, is compute-bound in nature, and is about to generate the first token. Decode is generating the answer one by one where if you have 500 words that you put in as a token giving some context for a

[02:04] particular task and it’s trying to generate the response the first token is generated and then from there the first token is fed back and 501 more tokens are fed in and the model spits out the 502 token all the 502 tokens generated so far are fed back and the 503 token comes out and so on. What Jev attempts to do is it skips the decode part all together and tries to condense the problem statement in the

[02:35] form of one forward pass or one pre-filled statement only. Now this has obviously lots of advantages. Your latency is going to be completely under control to start with but also your output is going to be predictable. It means you specify up front what you want from the model in the form of either options or boolean values and it gives back the exact thing that you’re looking for rather than you

[03:07] fumbling with syntax and language and trying to make the large language model predictable. So two things one forward pass no decode and predictable outcomes. Those are the two desirable properties you can say of Jev. Now coming to the interface of Jev you can post questions in three parts. So number one we have the choice

[03:38] where you can go all the way from uh almost like asking a multiple choice question. So either you have option A or option B or option C. A score on the other hand does the same thing but is ordinal in nature. So, you can say this is a good example where you have a nominal options and then the quality of that is you have ordinal options. What do you mean by that? So, uh if you if you ask for directions, east, west, north, south, those are nominal in

[04:09] nature. East is not greater than west, not greater than south, and so on. So, there’s no order you can maintain within choices. Choices are just uh exist orthogonally. Can’t be put in a certain order. Ordinal on the other hand can be put in a certain order. So, what’s the customer sentiment? See, they’re calm or frustrated or very angry. So, they can be ordered in the order of dissatisfaction, so to speak. So, um the APIs exposed by Jev for choice

[04:40] and score are different. And then there’s finally null, which is uh simply binary. So, you ask a yes or no question, you get the corresponding probabilities back. Now, it’s interesting the way it is phrased. So, nominal is slightly different than ordinal is quite different than binary. We will try to come up with a framework that caters to all of them, but we’ll also appreciate and respect the nuances that these three APIs possess.

[05:10] Um we’ll try and form a common backbone for everything that we do and from there build on top of it to uh segregate the nuances. So, uh we for our use case will build all three from one softmax per question, but I’ll definitely try and lay out where these can differ. We we come to what are some of the prerequisites for you to adopt this. Uh this is not for everyone. So, if you have a good grasp

[05:42] on the concepts of attention, on how KV cache works, on what’s a logic, what’s a softmax, uh you’re fluent with PyTorch, I think this might be for you. If you haven’t done any one of these things, you will find this video a little challenging to follow along. Some things that are good to have but are not mandatory is little knowledge of hugging face. Using a debugger, we’ll use the VS code debugger to go line by line on some of

[06:13] the concepts that we’ll try and learn. And then some of the modern aspects of transformers, the encoder, the decoder, and so on is really really good to have. So, these are definitely hard prerequisites. Uh If you’re not aware of these, probably this video is not going to add a lot of value to you. Um And then if you know these as well, it’s great, but it’s not mandatory. All right. What is not mandatory, however, is you do not need to own a GPU. That’s why

[06:46] the coding of this whole course is going to be very inference-heavy. We’ll talk a lot about the theory, but we’ll stick to the inference when it comes to implementations. So, you don’t want a GPU to do that. You don’t need any training experience or model training experience or or any kind of mathematics beyond understanding the softmax function. That’s it. So, with that, let’s jump right in. Here’s what we’re going to be doing. We’re going to split this whole thing into three phases. So, phase one will go through vanilla

[07:18] generation through LLMs. So, we’ll try and write a prompt that generates a JSON token by token. So, in this phase, we’ll go through the prefill and decode stages together. This will give you a fair intuition of the problem and bring everything into context for us to continue. From there, we will look at an encoder-only implementation. And then from there, we’ll look at the decoder only implementation. Uh Uh we’ll go back to a training run on how it works and then come back and finally do

[07:51] a compare and contrast on what approach to take when. Now, before we get into how Jev actually works, I think we’ll need a revisit of the vanilla decoding paradigm or the way they usually uh the way usually an LLM does inference. That will really put things into perspective on where we are headed next and how to reshape this whole thing to point to how Jev wants you to behave. So, we’ve got the tokenizer here and the

[08:22] model that we’re loading right off the bat. Again, we’re using pre-trained LLMs. We will be using pre-trained LLMs all throughout the course. Uh in this case, if I take a look at the load function, the model here is a quant 3.6B. Uh it’s tiny enough to fit in most laptops. So, we’ve got the model loaded here. And then we will check out what uh a naive prompt looks like. So, we’ve got the tokenizer here. The context

[08:53] looks like this. So, card ending 4412 made nine purchases in 3 minutes across four countries. The card holder called to say the card is still their pocket. Looks like a fraudulent transaction. And the schema that you will be looking for uh looks something like this and this will typically be our uh Pydantic object that you will pass to an LLM provider of sorts like an OpenAI API looking entity. Uh now, the schema contains

[09:25] something called priority. So, let’s take a look into that. It’s an enum where the choices are high and low. And then it contains a couple of more items, decision, which is again the choices are fraud or fraud review or clear. And then and escalate. So, you see these three have been carefully chosen to represent the three interfaces that Jev exposes. So, on the one hand you have priority,

[09:57] which can be high or low. This is the score API of Jev. Uh there’s the decision, which is again the subjective one. Uh and then from there there’s the boolean. So, these are the three questions that I’ve tried to frame which mimic um all of the interfaces that Jev exposes. And then we’ll try and uh kind of condense this into more simplified examples to see how Jev works later, but let’s first find out what this does. So, we’re going through a naive prompt

[10:29] function which simply takes all of this and puts it into a format ready for inference. So, I’m going to skip a bunch of these data handling because it’s not that interesting. So, once this processor comes back we will just take a look at the output and we will be able to clearly see that what were some of the things that were done under the hood

[11:00] in this. So, we’ll try and clear that out. So, if I look at the prompt I have tried to kind of put it in a format the way LLM providers like. So, if I were to print this, there’s an IM start token. And then from there the system message starts that you’re a precise data extraction system, blah blah blah. And then there’s a JSON schema that I want you to adhere to. And then the user provides the context. So, uh this is

[11:31] what is called the state in Gemma and then this is the user prompt which will be kind of put into state and then the questions will come in later. Yeah? So, in this case in a vanilla LLM decoding all I’m asking for is here is the user prompt, here’s the system prompt followed by the user prompt. Can you please give me the JSON back? Right? So, we even open that curly bracket up so that the LLM can continue decoding accurately.

[12:02] All right. So, there was a lot of stuff that we did in terms of changing the data to the right format but it wasn’t really uh, of any interest to us in our pursuit to understanding Jeff so I’ve skipped a bunch of uh, those processings over. Uh, so in this case all we’re doing is uh, tokenizing this whole prompt. So, all you will get is a bunch of um, begins which specify all of the token IDs of the corresponding tokens. All right?

[12:34] So, we’ll move ahead. Um, if it has an end of sequence token then great, otherwise we’ll set our own and this will be uh, a non-escaped again JSON curly bracket to signify that we are trying to finish our decoding by a generated JSON. Again, these are all uh, very bookkeeping stuff I would say. Uh, this is where the actual decode begins. So, we’ll try and clear the landing zone to understand this. Let’s take a closer look.

[13:06] So, our inputs our tokenized inputs are going through the model which will then spit out an output whose logits we will take, whose past key values uh, we will pluck out and this will become our KV cache and then from there we will look at the last token, go through its vocabulary, and pluck out the the argmax, which is basically the

[13:36] highest probability token. Now, if it matches one of our stop sequences, we’ll break. Otherwise, we’ll just keep continuing this, and it’ll be very clear once I kind of uh generate this and show you step-by-step what is happening. So, if we take the model, put it uh put the inputs through the model, and at all times we’re feeding in the cache. So, in this case, the cache is set to none, but then you see the the

[14:07] cache gets updated. So, uh what we can do here is if we have the same state or the same context, and we want to answer a bunch of downstream questions, what we can do is cache the state of the context, and then once I ask the first question, I can just retrieve that cache and process it by concatenating the question with that particular context. Um if I have a

[14:39] second question to ask, I can just grab the context again from the cache and attach the second question, and do this so on and so forth. So, I’m just trying to maintain a cache, which is just a best practice. All right. So, from there, uh the logits come out. So, just for perspective, I had uh let me take a look to the inputs. So, we have the input IDs that will give me an idea of the number of tokens.

[15:12] So, if I take the length of this, I get one, which is because it’s a nested JSON. So order it’s a nested array, I’m sorry. So there are about 130 tokens in total in this whole thing right now. All right. So from there once the outcome is out now you will see that if I access

[15:43] the logits then I will have 130 outputs and each token position will have the vocab size number of options to choose from. So uh the way the transformer works is if you put five tokens in, you will get five predictions out. Each prediction will contain the entire probability distribution of the vocabulary in question. So if you’re only looking for the last token, what you do is you throw

[16:14] everything away apart from the last prediction and then use that probability distribution to pluck the argmax, which is exactly what we’re doing here. So we’re updating the cache. Now from those logits we take the first dimensions. So since we’re only processing one batch if we just take one the first one and check out its shape, you will notice that the one in front is gone. And what we’re left here is the 130 by

[16:46] the 152k uh stuff that we want to look at. And then from there if I take the last element of this I’m essentially left with the probability distribution of the predictions of the last token. So if I take a look at the shape of this, then I have 152k outputs that I have to deal with. Now from there all I want to do is convert them to float put them through a softmax, and this is just dimension minus one.

[17:17] So, uh it does a soft max over all of these numbers. And then I’m going to take the arg max. So, if I really take this and do them one by one myself, so you will get a tensor of 130 uh rather 152k almost um probabilities. And then if you take out the the arg max of them, you get the token ID of the token that has the maximum

[17:48] probability. So, uh arg max give you the position, which is nothing but the token ID. So, I can actually decode this and get my answer. Let’s see. This is 330. So, this is the token whose uh which is So, this is the token that was predicted in this loop. So, if I just let this run for a couple of iterations and again,

[18:19] uh check out the token, the freshly minted token that was generated, you will see that uh it will keep changing over time. So, now it becomes priority. So, then I think we’ll probably have this thing um the the inverted comma closes along with the colon sign. And if we keep doing this for a few iterations,

[18:50] then you will see that the accumulation that is happening in this grown variable will reflect the answer that we’re looking for. So, this is exactly why you see chat GPT, the web interface, give you tokens one by one. This is because uh it’s looping through all of the previous context and generating these tokens one by one. So, it reckons that the priority of this particular ticket or particular context is high.

[19:21] And then, if we keep taking a look while iterating through a couple of turns of this, then you will see that it will subsequently reach a place where it has almost generated the entire JSON. So, if I So, if I take a look at this again, uh it’s done predicting the second one. So, it reckons it’s a fraud. And then, we’re left with escalate.

[19:54] has more tokens to generate. And if we come towards the end after it has uh finished its loop, then we will be able to see the final text that it predicted after all of the iterations of generating these uh tokens one by one. So, if we take a look at the text here, you will see that it has returned our final JSON. Uh let’s actually print that to see it very clearly. So, we’ll

[20:25] we’ll print the text so that it uh pretty prints it. So, then you can see that uh starting from this particular inverted comma to the word priority to then again choosing another token for this particular text and then generating token by token by token took a lot of time, which uh could have completely been avoided if we were smart about formulating our problem statement in a way that only let us um make predictable assumptions without

[20:58] getting into this vicious decoding cycle, at least for this problem statement. Right. So, there are a bunch of this uh bookkeeping stuff that takes care of uh trying to make sure that this ended up generating a JSON by looking at the escape characters, looking at the start, looking at the end, etc. etc. And then finally, once that post-processing is over, you’re left with an answer. Now, we saw how a decoder-only model was used

[21:30] to generate a response. We’ll begin now with one of the first formulations of our generative model. And this will be through the encoder backbone. So, what we’ll do is we’ll try and leverage an encoder model like BERT or any other variant of it in order to try and reformulate the use case so that we can avoid the decoding part altogether and still see if we can get

[22:01] all we want to get back. So, first things first, we’ll take an encoder-only model. Uh we won’t be doing any training. And we’ll try and see how well we can retrofit for this particular use case. And if not, what are some of the training approaches that can be taken. All right. So, the first thing first, you’ll see that we will encode the question here first.

[22:31] Now, this is a very unique approach. Usually, you provide the context, then you ask the question. Here, we are doing the opposite. We are starting with the question and then providing the context over here. Now, this approach is because attention, the way typically works, is it places a lot of importance uh to the beginning of the tokens in the attention block. So, we want to make sure we want to put that in. Uh experiments, I think, have been done with the reverse, and this has just ended up performing better. So, we’ll

[23:02] stick to this implementation. Now, once the question is asked, you have all of these options. So, in our case, it was either high or low or fraud or review or even yes or no. In this case, before every option, we will have a mask token. This is a special type of token that we will inject. And we will kind of sequence the whole thing. We’ll orchestrate the whole thing. And then, we’ll use this prediction effectively to gauge what option we can

[23:34] predict. So, if you have option B, we will inject a mask token against or just before option B. If you have an option C, we’ll do a mask token before that and so on. And then finally, we will have the entire context over here. So, once you see this, we are we are going to have to do this for every question. So, if you have a really long context here, um every time you ask a new question, the state has to be re-fed in this whole

[24:04] thing. So, you can ask, uh it’s an encoder model. Why can’t the state come first? Why can’t I cache it? And then, why can’t I just swap the questions one after the other? Well, here’s the problem. Uh encoders are bidirectional in nature. So, every time you actually change the question, you will end up completely changing the attention weights in every

[24:35] position of your attention matrix. Because of which, even if you flip this order around and have the state come first and the question come last, you will still every time you change the question, the bidirectional states the weights will change and you will have to re-crunch the whole thing over and over again. Um I hope you can see this. It’s a very simple interface, but then you can clearly see that for every question you ask, you will have to keep appending the state over and over again. So, you if you have a lot of tokens

[25:06] and a lot of questions to ask, this might not really be that scalable. But, given that it’s bidirectional attention, it will give you the maximum bang for your buck when it comes to every model parameter. Yeah? So, without further ado, we’ll dive into the implementation on how we’ve got a pure encoding.py. And we’ll set a breakpoint around here, and we’ll take this for a spin.

[25:36] Uh what we are interested is that we’ll have a state. We’ll have two questions just to see how it interacts with the multi-question setup. And then from there, we will see that how we are embedding the question and the mask and the context all in one. Now, one thing I have actively avoided doing here is I’ve not tried to batch the questions together, although you can. Uh I’ve really gone old school on doing for loops so that we can pick it apart and see how this is working. This is not the

[26:08] most efficient implementation. All right. So, first of all, um there’s this function called build, which takes in the state, which is nothing but the context. So, we’ll take a quick look. So, this is our context. Ticket 8812, charged twice for the same order, no reply in 4 days, cancel it and give me my money back, or I’m disputing with my bank. All right. So, the state along with the

[26:40] instructions, so each instruction will be a question. That’s how we are formulating it. So, this for loop is tending to the first question right now. Um the moment it loops again, there’ll be a second question. And then there are options here. Again, this question is yes or no. There can be multiple options as well. So, with this setup, let us get inside the build function to check what is going on.

[27:11] Now, it’s really easy. We take the instructions, which is the question, and then encode it first. So, there we go. We get the token IDs of these question back. And we are kind of doing it in a way that we put a classification token first and then the question and then a separator token. So, uh, once we do this, we will actually keep a tab on these separators and delimiters that we’re injecting so that we can pluck out what we want to pluck out in pursuit of our,

[27:43] uh, finding the answer. So, here we have the span, the span of the question. And then we have markers. Those are nothing but the options. So, we already know for the first question the options are no and then yes. So, uh, we’re injecting where I’m going to mark my mask token so that I can keep track during inference. You’ll see all of this in a minute if this is not clear.

[28:15] All right. So, we’ve got the markers appended and appended to the IDs. Now, if we, I think, if we track the IDs, we should be good. So, IDs was about just having the question, but then notice we’ve uh, put in the classification, uh, token and then a separator token once the question finishes. The mask token that we will observe and we will

[28:47] inject before every option so that we can track the masked tokens probabilities and do some things to it in order to gauge the strength of that option or the probability or the propensity towards that option for this question. Yeah. So, once we loop through this again I’m expecting there’ll be one more masked token followed by the yes option. So, let’s see that in action. Right. Once this is added in

[29:19] you will see that there’s another mask now catering to the yes option. Uh the options are all exhausted. The loop won’t run anymore. And then finally uh the IDs are constructed and returned. So, if I do a final check then you have the classification separator followed by the question then the separator the mask for the first option the second mask for the second option then a separator and then finally I dump the entire context followed by again a separator. So, these are uh

[29:51] very encoder lingo BERT-style MLM-style masked language modeling-style training paradigms. Uh and this is used in inference all the time if you’re using BERT. This kind of uh ordering of the tokens so we can differentiate between what to look at and what not. All right. Uh so, that’s the output of our build function. And then we get back and this is literally the LLM call. Uh so, we pass all of our input IDs. Now,

[30:22] the input ID here my new contains the question, the option, the state, everything. And then what I’m doing is I’m plucking out the last hidden state and then moving that into the CPU. So, we’ll take a look at it in what second. So, my model prediction is over. Here’s what it looks like. So, the hidden shape is 54 cross 768

[30:52] in nature. So, this is literally um each token has its own embedding that we’ve gotten. So, if you look at the number of tokens that we have was 54. So, the hidden state returns 54 embeddings for each of those tokens. Now, uh if we ask for the last hidden state, um what it will give us actually, let’s run

[31:24] this whole thing. So, if I run through this, it’ll do another model prediction. So, outcomes um 1 54 768. Now, if I ask for the first piece, then it will get rid of that batch dimensionality and just give me 54 by 768. So, if I check the shape of that, so that’s 54 by 768. And then, I’m converting this to float. And now, in the next step, what I’m

[31:55] interested in doing, I’m interested in plucking out just the query. So, again, the spans and all of these variables have been pre-made. We scripted a bunch of them to save on time. Let’s look at them now. So, if you look at span, it’s a tuple. The first element of span denotes the starting token or the starting position of the first question. And the nine here represents the end

[32:26] position of the first question. So, in fact, literally, if I do one to nine, and then try and decode it, I will get back the question itself. Is the customer asking for money back? Yeah? So, why did we start from one here? If I had start from zero, then I would have encountered the classification token, which I want to avoid. The question itself does not uh contain the token. Right? So, I will take

[32:57] the hidden states of the questions themselves. And if I look at it, um there were eight tokens in the question, so there’s 768, and I’m mean pooling them. So, I’m trying to come up with a 768 vector representation of the query itself. That is how you interpret this. Now, if I take this and I take the mean,

[33:27] you will see that I will be getting 768 numbers. I’ll repeat this. This is the best representation of the question that we have from the model. Because we’re taking all the tokens in the query or the question, and then have averaged out their embeddings to get the best representation of the question itself. >> [clears throat] >> All right. From there, we have got our markers, so we know for a fact that uh in the 10th place, again, we can

[33:59] check this. We’ve got the mask token, and if we look at a token before this, then you will see that uh or rather or rather, if we look at a token after this, you will see that it is succeeded by the option that we’re interested in. And now, if I take a look at the the 12th

[34:31] marker, then you will see that it is the marker marked for the yes option. So, we’ve got two options, one for each of um one mask for each of the options. And then uh we take up and find the hidden states of those as well. Again, we’ll get those two embedding representations of the two options. And now here is where

[35:01] our implementation is really going to be the vanilla part, and here is where a lot of variation happens in implementation. So, let’s try and understand this better. So, we’ve got the question here. And then um the mask token, then the option one, then another mask token, then the option two, and then so on and so forth till a separator comes along and gives me the entire context or the

[35:34] state. And then finally, gives a prediction. So, what we are trying to do here now is that each of these token generates a 768 uh embedding vector. Now, the question, if I take the span of the questions, the question itself will span a few tokens, and I average them out. Then this will give me a clean representation of 768 embeddings. Now, this mask token

[36:06] will also have a representation, so M1, and this will also have another representation, like so. So, you see the options span 2 cross 768, and then the query spans 768 cross 1. So, this is the option. This is the query. Now, one way to look at it is that I can make these two interact with each

[36:36] other. So, I can literally take a dot product and I’ll be left with two scalars and I can do something on top of these scalars, even take a simple soft max on top of these scalars and deem them probabilities. This is literally the simplest way to formulate a Jeff problem. Um no matter you have a choice, a null, or a score, you take all of these tokens, make them interact with each other. It’s

[37:07] bi-directional attention anyway, and then from there take the query vector, make a representation of it, take the masked option vectors, make them interact, and you can interpret the output as probabilities. Now, obviously, we are taking an out-of-the-box backbone with no training whatsoever. Uh this won’t work, but this serves as the bedrock. Now, what else can you do with it? Now, you’ve got option one, uh linear head.

[37:41] What I mean by that is instead of um taking these 768 vectors each of options, I can make them go through a linear head each and outcomes a scalar. So, that linear head literally can go 768 and outcomes one. So, if an option vector goes in, a probability, the corresponding probability comes out. So, this option

[38:13] um matrix here can literally go through a linear head and then outcome two scalars that represent the corresponding probabilities of each option. Second, I can do a full-scale lot of fine-tuning. Um and I can set an objective either by taking the way I formulated the problem where the option was trying to interact with the query, or I can fit in the linear heads and then have a full lot of fine-tuning. Option three, I do a full

[38:45] backbone retraining. Now, it turns out in practice what has happened is the embeddings that are learned, all of these 768 uh representations, they’re actually quite high-quality and high-fidelity, but they don’t know that they have to be outputted in a probability sort of a way. So, uh whatever number you get out of these won’t make sense, but all the signals in the world are already present. So, it really depends on how complex your task is, on how much data

[39:17] you have, or you’re willing to put in and invest on it. Um you choose one of these options. In its most vanilla state, you can get probabilities, but it won’t take you much. Uh you may have to put in a linear head and fine-tune just that linear head. You may have to do a full lot of fine-tuning or a full backbone retraining. So, with that perspective, we come back here. Now, we’ve got the option vectors. Remember, this is 2 cross 768. And then, we’ve got the query vector, which is 768 cross 1. So, you make them interact and normalize them.

[39:47] You actually put them through the softmax and you get probabilities. So, here in this case, if I take a peek, you will see that two scalars came out. Uh and these can be looked upon as the probabilities to answer that particular question. Now, I know they’re not very accurate. This is supposed to be a yes, whereas it’s only emanating 30%, but mind you, uh the embedding signals are very high-quality. This interaction needs a bit of a getting used to. Needs a bit of

[40:19] a slight fine-tuning, which is where our three options come in. Yeah? So, with this, I think uh we’ve already completed our encoder representation of how Jeff works. Um I hope it gave you a sense of intuition on what all you can do, and what all you have to do in order to make this work. Hope this helps. All right. Let’s actually check out what happens in the decoder world. Uh now, it turns out you can also use a decoder in order to formulate the

[40:50] problem in a way that it only takes you one forward pass, only the pre-fill stage, no decode stage, in order to achieve all of Jeff’s objectives. And here’s how it works. We are going to pack all of the state upfront. This is exactly the opposite of what we did with encoders, um because this time this can be cached. So, if a state, and since this is the causal mask, if the state has a KV cache existing,

[41:21] I can just keep swapping the questions here and reusing the KV cache just as well. Now, the other thing that I can do is if I have a large enough context length, I can actually pack as many questions and answer options that I can over here. So, imagine uh you have 32K worth of context, and then this only consumes 1K, the state. Now, just imagine you have 31K context to

[41:51] fill out as many questions and options as possible. Uh this is kind of evident that Jeff has taken a decoder approach, uh because it supports up to 5,000 questions over a single state. So, my best guess is they might be doing decoding, but uh again, they haven’t released anything, so can never be sure. Uh the first token or the the approach that we will be taking here is that we will formulate a problem in the way that here’s the context,

[42:22] here’s the question, and we’ll just leave it at a colon. What this does is it actually sets up the LLM almost as an auto completion task. And the moment it spits out a token, we will take a look at that token, and it will obviously have an entire probability distribution of its vocab size. We will actually focus on only the tokens that are there in our options,

[42:54] pre-trained or rather pre-built. So, if a choice has only got yes and no, we will only look at the probabilities of the tokens of yes and no in that first token decoding step. And we will ignore all of the rest, and we will treat them as probabilities and return them. Now, instead of that, you can also formulate the problem in this way, where uh you rewire the problem to almost like a multiple choice question. You have have

[43:26] options A, B, C, and D, and then you prompt the language model to answer what the answer is and nothing else. Either it’s A, B, C, or D. Now, based on what letter it chooses, uh you spit out the probability accordingly. Now, these but two approaches do not require training. Everything else does. So, uh we will stick with this part, and this can fairly be extrapolated because again, uh it’s just prompting the model to spit

[43:58] out one character, uh which are one of the options that you have. And then you can have again, a decision to option uh pointer head, so it goes through a linear layer again in order to do an interpretable probability distribution. This is what uh Jeff has implemented. This is an open-source alternative to Jeff, and then Cloudflare came up with Clef, which is basically a pooled span implementation of the whole option plus uh a particular linear head.

[44:30] So, uh it takes into account the entire option for those questions, so it can never really go wrong in the case of duplicates. So, just to be clear on what I meant by that last part, uh is this legit can be, let’s say, a question. I just made it up. Uh option one, fraud, and then option two can be fraud review.

[45:03] Right? So, if you have and it’s uh a perfectly formulated Jeff-style task, where you have option one and option two, notice that the first token are exactly the same. Now, this is where our simple implementation will go for a toss. Uh it won’t know the difference between these two, because the first tokens are literally the same, and it’ll probably spit out 50/50. So, what I’ve done in my implementation is handle it so that if there are duplicate tokens, or if there are the same tokens, we’ll just throw an error prompting the user to change the

[45:34] way they formulate the option. So, uh this can literally be just review instead of fraud review. And then uh that thing comes for free, right? So, uh again, let us lots does not have that problem. If you just call it A and B, and then ask the LLM to give you one of these letters, I think it should be fine. Uh but then uh pooled span especially takes that to the extreme and avoids it completely, even if there are completely duplicate options, uh this particular implementation has

[46:05] the scope to disambiguate it. We are again not going to go through any of these three implementations. We’re only going to stick to the first token task and see mainly focus on how the cache can be reused and how the question swapping occurs. This in itself is going to be a challenge. Before we dive into the code, I want you to have an understanding of how we are going to formulate this causal mask block because this is really going to be important for you to visualize in order to understand how we can pack multiple

[46:35] questions in a single sequence. So, let’s say we have a question. Um uh we have some context. Um My name is Vivek. Now, from there the question becomes Oh, what is my name? And then maybe uh What is

[47:07] or rather we formulate it in this way. Uh the option can be uh Vivek or something else. And What is my surname? And can be anything. So, option A and option B. Okay. So, the way we’ll formulate the problem is this. It is causal mask after all.

[47:38] So, uh token one, token two, and so on. So, if you really visualize the attention matrix over here like so. The first token can only attend to itself and it cannot attend to anything else. Now, the second token can attend to itself and the previous token and so on. This continues, right? So, uh at every step each token can only look back and not look in front.

[48:09] Now, this in the context of when you have multiple questions and you want to pack all of them in a certain context, gets a little tricky. All right. So, let’s say uh the question is three tokens and let’s say for arguments uh rather the state the state is three tokens. For arguments sake, you can ask one question along with their options in the next two tokens.

[48:40] And question two also uh takes two tokens. Again, for so that we can actually see the full-blown matrix. So, what will happen is you’ve got uh state one, state two, state three, and then Q1, Q 11 12, and then Q21, Q22. And then the same thing goes here, S1, S2,

[49:11] S3, Q11, Q12, Q21, and Q 22. Now, the way you want to pack this is that this will be definitely uh vanilla attention as you know it, typical masked attention. Causal mask, yeah? Now, this question should be able to see the entire context. So, I’ve got no problem continuing the causal mask that we are building over here, right?

[49:45] We are good so far. Now, what should happen to question two? Now, ideally, question two should have all the access to look at the state, but why should question two have any access to look at any content of question one? I mean, it is theoretically possible to expose question two to questions question one’s context, but think about what you’re doing here. You’re actually poisoning the context with a question you’re not even supposed to know or look at.

[50:16] Uh what this will introduce is that if you ask questions in a particular order, you will get different answers. So, it won’t be very reproducible on your domain. Uh very interestingly, as far as I’ve studied Cloudflare’s clef, has done this implementation where they haven’t masked, and it uh just doesn’t make any sense to me. Uh because the once you mess up the order of the questions, you will get different

[50:46] outputs, I’m sure. So, if you look at this, uh question one, or rather question two, should have all the access to the previous state, should have no access to the first question, and then should have access to its own token, of course. So, it has access to the state, has no access to the question, has access to itself. And this is how

[51:17] we’re going to have to So, the state will be its own block, and then when it comes to the question, the question will have access to its own block and the state. And the next question will have access to its own block and the state. And this will be completely masked out. So, constructing this mask will require some array gymnastics, which we’ll soon look at. And that’s why I wanted to really focus on

[51:48] uh the first principles and the core aspects of constructing this rather than uh looking into a varied set of implementations and confusing ourselves. Once you understand this fundamental, I think the different varieties of implementation, the different uh tweaks in the approaches that the different providers have taken will be easily accessible to you. Without further ado, let’s dive right in to the implementation. So, we’ve got the pure decoding script

[52:19] here. And since we have some of the perspective on the theory, we’ll quickly look at the state. So, this is the context you’ll be provided. So, “Just twice want my money back.” And the questions are essentially two. If I look at the first, is the customer asking for a money back, yes or no? And which team owns this? Is it the billing team or the technical team? These are the two questions we’re interested in asking. Uh again, this can easily be extrapolated to a score

[52:49] question as well. I’ve only included null and choices in this matter. Yeah. So, the first things first, we’ve loaded our model. This is a decoder-only model, so we’ll use the exact same model as we did in the vanilla decoding stage. So, this model is going to be a Qwen 3.6 billion parameter model. Uh and then from there, we are providing the token, the model, the state so that we can get our cash back. And this is one of the most desirable properties of

[53:19] decoder-only. I can pre-cash the state, and then I can keep on answering questions. Either I can pack those questions together, or I can just go on a loop answering them one by one. Yeah. So, we’ll go slightly inside the prefill stage to take a look at what is going on. And then you’ll be able to see that the entire state is getting tokenized like so. Uh And then I’m actually doing a model inference over here. And output with the past key values so

[53:50] that I can get my KV cache for this particular state. So again, if I take a look at the IDs that went in, there are 38 tokens. So the KV cache uh typically as far as my understanding goes, it will have some layers, some KV heads. So all of that put together and of course uh key values so there’ll be two. So two cross the number of layers plus the number of times the number of heads times the dimensionality uh of the

[54:21] embedding. All of that will matter when it comes to plucking out the KV cache. So uh the dimensionality, if I take a look Oops. Uh this is Let me take a look at the length. So there are 28 layers in this model. It tells us something. All right. And then each layer has this uh KV cache.

[54:52] Or rather, how do we access the first value? I think we do do it by the layers property. This is the first layer. And then here I can take a look and hunt for that particular uh So there you go. So we’ve got the keys and the values as tensors here. So if I take a look at this shape I will have the batch size. So

[55:23] this has eight KV heads, 38 tokens, and 128 dimensions. And there are a total of 28 layers. So this times 28 times two because this is just the keys, we also have values. All right. So, from here uh we pre-cash our state, so to speak, and now we are ready to answer questions. So, we go from Let me run it till here, and then we’ll

[55:53] go deeper on this particular function, answers. Right, so uh we’ve dealt with the state or the context. Now, we are going to deal with the questions. So, we’ve got the questions here. It’s a list of um tuples. Each tuple is a question and a list of answers. Or rather, a list of options. All right.

[56:24] So, this is where we begin the whole exercise of trying to make sure the right tokens are um getting served by the right tokens. This whole question two not being able to see question one and so on. So, we’re constructing this question mask. Let’s take a quick look. So, you’ve got the width of the segments. Uh this will contain the boundaries of the questions themselves. So, if you see um

[56:56] if I have a total of 35 tokens that comprise the questions. So, the state has been cashed already. We don’t need to worry about it. These are the 35 tokens that contain the two questions along with their options. And we’ve seen that the roughly half of them are question one and the other half is uh question two. So, the segments denote the actual index of the question.

[57:26] Right. Convert them into a torch tensor. Now, uh we construct a basic causal mask with the A range and then we make it uh, greater than equal to its own transpose so that we can come up with our causal mask. So, if we take a quick look, the causal mask is simply uh, the first element is true, the first two elements of the second row is true, the first three elements of the third row is true, and so on. This is the vanilla causal mask.

[57:59] Uh, we’ll need to do more stuff, as we’ll see, in order to get that uh, whole question-to-question abstraction that we want to achieve. So, whether it is the same question or not is dictated by the segments, and we’ve got the segments uh, in a torch tensor called seg. So, we’re going to do the same thing, uh, but with an equality clause to its transpose, and in this way, I’ll be able to gauge whether uh, two tokens are part of the

[58:29] same question or not. So, if I just go through uh, the same question tensor that I have, let’s see what that spits out. So, you see, you will see that the first few tokens here are all true with each other, and the last set of tokens here are all true with each other. So, there’s clear segmentation of these are the tokens of question one, these are the tokens of question two. So, now, if I take that and make it interact with

[59:00] the causal mask that we constructed in the previous steps, some interesting properties will come out. So, we’ll just wait for that to happen. Yeah? So, in the next step itself, the visible should be the concatenation of torch.ones, the width of state length, along with the interaction of the same question and the causal mask that we’ve constructed. So, this will give us that uh, the desirable state of what we wanted to achieve. And this I think loops through for

[59:30] questions. Okay, so we’ve got the visible tensor with us. Let’s take a deeper look. So, we had the causal mask, we had the same question mask, we made them interact with each other to come up with this. All right. So, we can see that uh I think this entire shape is 35 cross 73. So, there are 73 tokens in total which constitutes the state and all the

[1:00:03] questions. These have been packed together. Now, since the first few tokens have been cached in already, I just need to care about the 35 tokens that comprise the question. So, if I take a look here, what I have is this piece of the puzzle entirely. So, I have all the questions with me. I have this part. Hence, this is 35 cross 73. I don’t have the top part because I have already cached that in. So, this part is already

[1:00:34] cached in. I’m just interested in this entire part. And hence, I have this piece with Hope that is clear. So, the moment you see visible, you will see that they resemble the state. The first part is the state where everything is true. Again, if you look at this part, all of it is dense true. And then the only interesting decoding

[1:01:06] part starts once it crosses over the number of states over to this. So, if I were to really slice and dice this to ignore maybe the first uh 38 tensors which contain the context. So, let’s actually do that. So, from the 38th token onwards, you will already see that we’ve got this causal mask running because we’ve exhausted the state which had 38 tokens in it. And

[1:01:39] then from there you will see that uh everything in the bottom left is false because these are the tokens of the first question and we are taking care of the second question now. And then everything is just causal for question two. Everything is causal for question one here. Everything else is false on the top right. Everything else is false in the bottom left. So, we’re trying to maintain that mask uh that we saw here. That neat mask where uh here everything has to be turned on well. Everything here has to be turned off. And everything here has also got to

[1:02:10] be turned off and so on, right? So, only we look at um questions looking at their state. That’s what we want to achieve. So, hopefully this was clear on how we are constructing this mask. It’s a little different from the vanilla causal attention mask. So, that’s why it had to be kind of deep dived into. But once we get the mask back, everything else remains the same. We’re going to go through the same journey that we go through in a vanilla decoding state where we only just do one

[1:02:43] predictions and one prediction only. So, we’ve got all of these segmentations and all to boot. Um we’re going to pass that attention mask that we constructed along with the IDs that contain all of the state, the question, and so on. We’re going to get our uh we’re going to pass our uh KV cache here because we’ve already done that for question. Um we’ll pass in the position IDs here as well. And

[1:03:13] if you take a look here, you will see that it spans all the way from 38 to 53. So again, keep in mind uh while we are feeding it a fresh tensor, a cache of 38 tokens have already been fed. So the correct positions needed to be uh filled in so that it can uh calculate the position embeddings and rope and everything properly. Yeah? So we get the logits.

[1:03:48] And we’ll just run it till here. So we get the logits for this. And if you take a look at the shape of the logits, it’s typically what an LLM should do is that the number of tokens that you passed, it will give you the exact same thing back, but along with um for each token position, there’ll be a vocab size uh which will contain the distribution of the probabilities at each position. Now, we will only be interested in the last token because we are assuming that the

[1:04:19] last token itself contains our non-duplicate answer. So we’re going to uh dig down here. So for every instruction choices decision option IDs in questions, decisions, and candidates, uh let’s actually take a quick look on what these arrays are doing. Questions is simply the text of the questions. You’ve seen this before. Uh decisions, 18 and 34, is actually uh the

[1:04:51] end decision point of each question. So um if I take it take a look at the first 18, uh Uh, you will see that it contains the question along with the options. And if you take a look at everything else from there,

[1:05:23] you will see that you’ve got the second question as well as its options. So, decisions are being taken based on those questions and their options. And of course, we have the the candidates. And these are uh, the token IDs for yes or no and billing and technical. So, we’ll take a quick look here as well to the candidates so that we can assign probability. So, this one is a yes, this one is a no, this one is a yes. So, 902 is a no, 9834 is a yes, 30531

[1:05:55] is billing, and uh, 10916 is technical. So, that’s how it’s um, shaping out to be. So, we’re going to enumerate over this zip of questions, decisions, and candidates. Now, obviously, we only care about the decision points. So, we’ll go right there. This is where question one ends, and it was able to look at all of the state before. And this is where, in the next loop, question two will end, and it will also have looked at all of the state and

[1:06:27] context before that. So, that’s the last token that I care about, whose logits I’m going to grab and convert them into probabilities. And they will represent in my case, the corresponding probabilities of the options themselves. So, let’s take a look at them. So, we’ve got the probs here. Quick look. So, we’ve got, for the first question, 95% confidence that

[1:06:57] the customer is asking for his money back. So, you see, with the decode only, it works right out the bat. Uh, unlike embedding, switch needs some sort of fine tuning. All right. So, for the second question, we’ll go through the same journey where uh which team owns this? Is it billing or technical? And then the probabilities we’ll see here for those two options. And if there are three options, three probabilities come out. This is not a problem. So, 97% confidence that it is uh of the billing

[1:07:29] team. And about 2 and 1/2% of the rest, which means it’s from the technical team confidence level, right? Uh so, that we go from here to the end. And practically once we’ve reached this stage, uh you will see that I’ve got another line of code here which says, “Answer, how angry is the customer?” Calm or furious. And this is a score-style question that you can run it

[1:08:00] yourself. I didn’t bother running it. But the magic of the decoder-only style I wanted to show you is that this does not require recomputation of the context or the state. It has been precomputed. So, you can keep adding more questions here. And changing them, changing the options, none of that matters, none of that will change. Uh it will all be the same. So, the the state will never change. It has been cached in. The questions

[1:08:32] you can uh in fact have a follow-up with the user uh as well instead of batching them up. So, I hope that gives you a fair understanding of how the decoder-only JAF inferencing works. Uh as a bonus, we will try and cover how the training works as well. Uh we will not be going through code or going through um any of the data sets and stuff like that. We will, however, formulate the training objective. We will look at the loss function and how it’s computed and

[1:09:03] what are some of the varieties there and we’ll you’ll really get a flavor of RLCD in theory. So before we dive into the training part, I really wanted to tell you what some of the shortcomings of the method I show you are and how you can kind of avoid them easily. So when two answers start the same way and I’ve already shown this to you before if two answers start from the same token our implementation will fall apart. Now

[1:09:34] a free thing you can do in order to still retrofit this to the simple vanilla approach that we have taken is simply rename them. If not we need to score every token and this will take us one extra pass. So instead of just one decode step, we might need to go through multiple decode steps and then do a do a mean pulled scoring strategy. Or you can really [clears throat]

[1:10:04] condense every option and encode them into letters like A B C D just like MCQ questions are formulated and answered. This will also make the collision go away. And of course you’ve seen this before with encoders you have the mask token stapled just before an option. Because it’s positional in nature, it never confuses an encoder. Duplicate tokens in the choices are only a decoder problem, not an encoder

[1:10:34] problem. Yeah. So with that we’ll get to the training part. We will go through examples of how training works, but we won’t be actually showing you a training run and actually coming out with a model. Um leaving that as an exercise. Some of the extra scripts or bonus scripts that I will share as part of the GitHub will contain those training steps, but also they will be with synthetic data. So, it will be of very little real world use.

[1:11:07] But I’m sure you can take them as template and reconstruct it for your own use case. So, with that, we’ll look at one training step worked out. So, the way these trainings are formulated is through teacher student distillation. That’s pretty much it. So, either you have pre-labeled data or you have a teacher model that has the ability to give you some sort of probabilities. So, let’s say

[1:11:38] for our previous question, is this a billing problem or a technical problem or account problem or whatever, you have two option three options, and the teacher model says 0.7, 0.2, and 0.1. This is the probability distribution. Now, our scores, you can think of them as logits or scalars coming out of the linear head or whatnot. So, they haven’t been soft maxed yet. These are the scores. The raw scores are +1, +0.8, and +0.20.

[1:12:12] Now, our answer our probabilities are 0.44, 0.36, and 0.19. Now, notice that they maintain rank order, but still, I would like our answer to be closer to that of the teacher. So, what do we do now? Now, we use this paradigm called GRPO, group relative policy optimization. It’s a concept in reinforcement learning. Um

[1:12:43] if you haven’t gone through GRPO before, you should probably get an intuition into it. I think it will dilute our focus by a lot if we try and explain what a GRPO does, but in essence it is trying to follow multiple trajectories, taking their average, and if a trajectory follows a good path or generates a really high fidelity answer, we subtract the mean from them, and what’s left is something called advantage. And we use that advantage to construct a loss, and then back

[1:13:13] propagate to ensure that the LLM spits out the good answers more than the bad ones. That’s reinforcement learning in a nutshell. Uh in this case, we don’t have multiple trajectories because, again, we’ve constrained our model to generate certain token probabilities only. So, we’ll have to manufacture them out of thin air. So, what we do is we take these scores. So, we’ve got these scores here. So, we’ve got 1.8.2, and we add jitters. We add random

[1:13:45] perturbations to these scores. And let’s say trajectory one, which is certain kind of random perturbation, yields these scores. Uh on the second leg, we add another set of random perturbation, and it does this, this, and this. So, instead of the original scores, we’ve morphed it and added added random jitters to come up with these particular scores. Now, if we convert them into probabilities, they look something like this. Now,

[1:14:17] the first trajectory is something that I like because it moved towards the direction of the teacher while still maintaining rank order. So, I would grade this higher than the rest, and then since uh each of these things receive a grading, their average will be subtracted out, and you will see that this has positive advantage. So, this is literally what I was trying to explain is the advantage. So, anything having a positive advantage will have a lesser loss and will be

[1:14:47] favored to more compared to the others. So, this will nudge our student model towards the right direction, towards uh mimicking the distribution of the teacher, so to speak. Yeah? So, just to show you some of the calculation. So, uh these are the rewards that we got. So, -0.33, -0.52, -0.5241, -0.4154, and they average out to become

[1:15:18] -0.4498. Now, if I take the reward and subtract the mean from it, so I’m essentially doing -0.3380 -0.4498 divided by 0.0901, is again the standard deviation of this whole distribution, and out comes the answer, which is I’m literally calling it the advantage. So, these two trajectories have a positive advantage, while these two have a negative advantage, and if you look at their answers themselves, you will see why they have positive advantage intuitively speaking. So, we had a 0.44, which is our baseline. Uh it bumped it up ever so slightly, while decreasing this, while further decreasing this, or maybe uh this kind of increased a little, which is So, there is an overall

[1:15:48] advantage of gravitating towards this trajectory. Now, this one has maintains the rank order beautifully, and also gravitates more towards the teacher model, hence it just has more advantage. So, the the scoring mechanism has been come up in this way that uh with these trajectories, the better answers have more advantage, which then affect the loss more. Yeah? So, the one last question that is remaining here is if you look at their answers themselves, you will see why they have positive advantage intuitively speaking. So, we had a 0.44, which is our baseline. Uh it bumped it up ever so slightly, while decreasing this, while further decreasing this, or maybe uh this kind of increased a little, which is So, there is an overall advantage of

[1:16:19] gravitating towards this trajectory. Now, this one has maintains the rank order beautifully, and also gravitates more towards the teacher model, hence it just has more advantage. So, the the scoring mechanism has been come up in this way that uh with these trajectories, the better answers have more advantage, which then affect the loss more. Yeah? So, here is where did we magically get these scores? So, let’s explore one of them on how we get or we

[1:16:51] calculate one of those scores. So, this score -0.3380 just of the first trajectory is an amalgamation of multiple scores. So, we’ve got the log score. We’ve got the spherical score. It’s a combination of those. So, uh the log score is trying to answer the question, “Did you put probability where the teacher put it?” Spherical is that does the whole answer

[1:17:21] point the same way as the teachers? So, overall there has to be a trajectory towards approaching the right answer, but there also needs to be an approach where if a teacher model is scoring a particular option high, the student model should score it equally high. So, it’s mimicking the distribution on the one hand, being directionally consistent on the other. That’s the intuitive part. So, we’ve had uh the way of implementing it is that we have the teacher probabilities here 0.7, 0.2, 0.1. We have the student

[1:17:53] probabilities here 0.52, 0.31, 0.16. I take the log of this, which comes to this. And then I weigh these logarithms by the teacher probabilities. So, the more probability it has for a particular option, the more say it will have towards the overall sum. Now, once I have this sum, uh I’m going to put it as a particular term, and then I’m going to sum it up with the spherical score. So, how does spherical score work? Uh it’s simply again

[1:18:25] probabilities weighted by the probabilities of the teacher. So, we’re not only taking the log of the probabilities weighted by the teachers’ probabilities, We’re also taking literally the probabilities weighted by the teachers’ probabilities. So, we’re taking 70% of the first probability, the student probability, and so on and so forth. We’re normalizing all of that. And then, the final score comes out to be this, which we are

[1:18:56] injecting here. So, these two put together constructs the reward for a particular trajectory. Now, uh this is where the nuances come into the picture. Um 99% of this is very clear. So, there’s a teacher uh model that spits out the probabilities. There’s a student model that’s trying to adhere to it and trying to mimic it. That’s the log score and the spherical score.

[1:19:27] But, there’s also one last push only for one particular interface. So, remember uh Jev has the score, the null, the choice interfaces. Just for the score ones, another implementation that we have is something called RPS or ranking probability score. So, what we are trying to answer here is that score will have levels or it’s ordinal in nature. So, let’s say I have a question where

[1:19:59] the answers can be um frustrated or very angry. Now, that’s a spectrum. It’s going from almost You can visualize it as going from left to right. So, let’s say the truth here says that the customer is very angry. So, it’s zero for calm, zero for frustrated, and one for very angry. Now, if I say frustrated versus if I say calm, you will notice that saying I’m frustrated is slightly better.

[1:20:33] It’s I mean, I would say that the model is slightly better calibrated than another model that says calm. Right? So, and this only applies to ordinal predictions. So, for this if I uh if I actually predict this .1 .8 .1 then the log score or versus .8 .1 .1. So, if I just reverse this uh log score will give me the exact same

[1:21:04] score. But, you see the RPS will differ. The uh probability score the running probability score uh will differ. So, it’s the squared gap between the running totals averaged over a minus one of them. So at every step I am going to uh take this RPS only for the choice questions and feed that back into my loss function in some way.

[1:21:37] Yeah? So, the loss function is finally equal to the log score 75% of the spherical score and you can basically weight fully if you want to minus the RPS. So, this is the grade and in order to construct the final loss function um I’ve all I’ve actually seen things like uh cross entropy plus this RL loss. So we’ll go with full cross entropy plus the RL loss

[1:22:09] and by RL loss I mean it will contain the spherical plus uh the log score. And if it is a score style it will also have the RPS embedded in it. So, that is the the core proposition of how RL CD works. So, there’s a full-on cross-entropy, and then there’s a full-on RL component.

[1:22:41] And for that, the trajectories are generated uh with the help of GRPOs by adding random jitters, and it tries to come up with two loss functions combined to come up with the final RL loss, and then finally, uh you back-propagate that over whatever you’re training. You can choose to train over a simple linear head, or you can choose to train over uh a Laura, or you can uh do full-scale uh backbone fine-tuning as well. The choice is up to you, it’s up to your appetite, it’s up to the accuracy metric

[1:23:13] that you want for your use case, and so on. It really depends. Quick note on the implementations and some of their comparisons, advantages, and disadvantages, and so on. Uh so, we’ve implemented both paradigms for Jev, uh the encoder-based and the decoder-based. So, uh some of the dimensions along which you should evaluate whether to uh take the encoder approach or the decoder approach are as follows. So, if the state is to be kept in between questions, the encoder has to reread it every time, but the decoder

[1:23:45] can read it once and cache it. So, if you have a lot of questions over a finite set of context, I think decoder is the right approach. Now, cost of questions pile up, this grows with every question. Um this remains flat, only the questions are new. Uh if you want more context length, if you have longer documents to process, then decoder is the right approach. State read with the question in view. Uh again, since this is uh bidirectional implementation that encoders do, it will

[1:24:17] always have a slight edge over uh causal mask or causal attention. Options are several tokens. Now, since the marker sits in front, it’s very easy to have uh mask tokens injected before those options and have them scale well. Uh again, with decoder, it will be slightly disadvantageous compared to encoders. Several labels at once, we can easily replace the head with a sigmoid. We are

[1:24:47] only stuck with softmax, so we’ll really have to um do a little bit of model engineering, tweak the architecture a little bit in order to make this happen. Um >> [clears throat] >> now, typically lower uh number of parameters in encoders do a better job compared to their counterparts. So, if I have a 400 million parameter encoder model, uh I would need a more capable decoder model in order to do the same job. So, that’s the core aspect. So, if you’re really crunched for

[1:25:18] uh memory or obsessed with speed, uh you really have to take into consideration all these dimensions along with your domain and problem statement in order to make this work. Uh and then finally, what you have to write is uh one sequence for question versus mask positions and cash. This is the engineering tradeoffs that you’ll have to uh take care of. Yeah. So, I think that summarizes all uh what we have tried to bring together here. Hope you enjoyed and really have fun learning this. Thanks a

[1:25:49] lot for your time.