Documentação

Pesquisa de desempenho do Laya MLX

Data da pesquisa: 2026-09-19. Alvo: Apple M3 Max, 40 núcleos de GPU, 128 GiB de memória unificada, MLX/MLX Metal 0.32.2. Esta é uma revisão estática do runtime nativo, da implementação instalada do MLX, da documentação oficial e do JSON de benchmark existente. Nenhum benchmark de GPU ou inferência de modelo foi executado para esta pesquisa. Nenhuma das otimizações propostas abaixo tem um ganho de velocidade medido neste relatório.

Os primeiros experimentos devem ser a compilação do modelo inteiro e o agendamento representativo de lotes, seguidos de multiplicação de matrizes quantizada seletiva. Estes abordam o trabalho repetido dominante. Um kernel especializado de atenção local é um projeto de prazo mais longo crível para entradas longas. A poda exata da última camada da cabeça de decisão é viável, mas sua economia aritmética no modelo inteiro é de apenas alguns por cento. Grandes melhorias sem mudar o checkpoint exigirão melhorar o backbone denso, eliminar requisições genuinamente redundantes ou encontrar um gargalo de implementação medido; simplesmente substituir uma ativação ou ativar outro flag de atenção provavelmente não bastará.

O que as medições existentes estabelecem

Os números a seguir são latências medianas ponta a ponta existentes, incluindo preparação do prompt e formatação do resultado, com conclusão sincronizada na GPU, cinco warmups e 50 iterações medidas. O carregamento e o download do modelo ficam de fora. O benchmark permite um lote de 64 perguntas; o padrão do runtime público é 16, então o resultado de 50 perguntas não é a configuração padrão da API.

Checkpoint / precisão Curta 1 pergunta Curta 10 perguntas Curta 50 perguntas Longa 1 pergunta Longa 10 perguntas
Laya MLX FP16 13.421 ms 71.068 ms 336.030 ms 44.927 ms 420.987 ms
Laya MLX FP32 15.954 ms 98.820 ms 450.712 ms 61.331 ms 534.242 ms
Laya Torch MPS FP32 original 24.918 ms 95.265 ms 497.856 ms 65.581 ms 586.594 ms
Multilíngue MLX FP16 7.390 ms 27.386 ms 127.565 ms 37.635 ms 389.487 ms
Multilíngue MLX FP32 7.988 ms 32.337 ms 151.387 ms 47.208 ms 451.331 ms
Multilíngue Torch MPS FP32 original 19.349 ms 43.158 ms 194.171 ms 52.939 ms 534.492 ms

Fontes: Laya FP16, Laya FP32, Laya MPS, multilíngue FP16, multilíngue FP32 e multilíngue MPS. As entradas longas contêm 512 tokens para o Laya e 1024 para o multilíngue; portanto, comparar suas latências de entrada longa não é uma comparação com o mesmo comprimento de sequência. Os comprimentos curtos com padding são 93 e 91, respectivamente. As comparações com Torch devem manter o rótulo FP32: elas combinam uma mudança de backend com uma mudança de precisão quando comparadas ao MLX FP16.

Há variabilidade significativa entre execuções. Por exemplo, a execução multilíngue FP16 de 10 perguntas longas tem p50 389.487 ms, p95 462.319 ms e máximo 619.663 ms. Sua mediana de forward curto de uma pergunta é 8.023 ms, enquanto a mediana ponta a ponta medida independentemente é 7.390 ms. Subtrair essas medianas produziria um tempo de pré-processamento negativo, sem sentido. Os arquivos atuais não isolam tokenizer, despacho do Python, kernels individuais de GPU ou custos de sincronização. Eles estabelecem linhas de base úteis, não um diagnóstico de gargalo em nível de kernel.

Os relatórios de validação registram 63/63 de concordância de argmax para cada um de três checkpoints em FP32 e FP16, e 100 chamadas repetidas finitas e determinísticas por variante: 378/378 concordâncias de resposta e 600 chamadas repetidas no total. São verificações de regressão sobre um pequeno corpus de fixtures, incluindo perguntas repetidas. Não são evidência de que futuras mudanças de quantização ou arquitetura preservem a precisão geral na tarefa.

Por que a multiplicação densa de matrizes merece prioridade

A implementação do modelo aplica projeções QKV e de saída, um MLP de encoder com gate e duas camadas convencionais de transformer na cabeça de decisão a cada token com padding. Sejam D o tamanho oculto, I o tamanho intermediário do encoder, N o número de camadas do encoder e H o número de camadas da cabeça de decisão. O número de pesos de matriz usados por token nesses blocos é:

A = N * (4 * D^2 + 3 * D * I) + H * 12 * D^2
dense FLOPs per batch ~= 2 * B * L * A
dense attention FLOPs ~= 4 * B * (N + H) * L^2 * D

Essas estimativas contam multiplicação e adição separadamente e excluem normalização, ativações, embeddings, pontuação, mascaramento, movimento de memória e sobrecarga de kernel. São um modelo aritmético, não um perfil de runtime.

Família de checkpoint D / I / camadas do encoder Pesos de embedding de token Principais pesos de matriz por token A Camadas globais / locais do encoder
Laya / decisões tipadas 1024 / 2624 / 28 51,576,832 368,312,320 10 / 18
Multilíngue 768 / 1152 / 22 196,608,000 124,452,864 8 / 14

Os aproximadamente 322 milhões de parâmetros totais do checkpoint multilíngue incluem 196.6 milhões de parâmetros de embedding. Apenas as linhas de embedding selecionadas são reunidas para a inferência; isto não é uma projeção do vocabulário inteiro. Sua principal carga de trabalho de matriz por token é aproximadamente um terço da do modelo em inglês, apesar de suas contagens totais de parâmetros parecerem muito mais próximas. Isto é consistente com a diferença de latência medida em lotes curtos, embora não prove um gargalo de hardware específico. A quantização apenas do embedding reduziria predominantemente o tamanho dos pesos residentes, especialmente para o multilíngue; ela não precisa melhorar a latência de inferência.

Com o caminho de atenção densa atual, os produtos de atenção representam cerca de 1.5% dos FLOPs modelados em L=93 para o Laya, 7.9% em L=512 para o Laya e 23.3% em L=1024 para o multilíngue. Sua fração do tempo de relógio de parede pode diferir substancialmente. Um profiler deve distinguir kernels de matriz, kernels de atenção, kernels elementwise, construção de grafo na CPU e lacunas de ociosidade antes de se comprometer com trabalho em Metal personalizado.

Experimentos priorizados

Prioridade Experimento Melhor alvo Principal trade-off / condição de aceitação
P0 Fazer profiling de uma forma curta e uma longa; compilar o modelo ou os blocos do encoder Latência de requisição única e despacho do Python Manter saídas idênticas dentro da tolerância de precisão existente; medir a compilação de primeiro uso separadamente
P0 Ajustar o batching pelo orçamento real de tokens e pela distribuição de comprimentos Muitas perguntas distintas e tráfego de comprimentos mistos Otimizar a vazão sujeita a um orçamento de latência p95 e memória; levar a fila em conta
P1 Quantizar camadas lineares selecionadas do backbone, começando em 8 bits e depois 4 bits Tráfego de pesos e, potencialmente, inferência densa Portões de qualidade e calibração; a velocidade real no M3 Max pode regredir
P1 Fazer cache da tokenização de state compartilhado e de templates de pergunta estáveis Muitas perguntas compartilhando um state ou rubricas repetidas Identidade exata de tokens; cache limitado; separar resultados com e sem cache
P1 Calcular apenas as saídas necessárias da camada final da cabeça Toda carga de trabalho, especialmente sequências mais longas Poda exata de dependências; economia aritmética modesta no modelo inteiro
P2 Atenção genuína de janela local com limites de tile Cargas de trabalho de 512/1024 tokens Complexidade de novo kernel; preservar a semântica de janela bidirecional e de padding
P2 Fundir residual/norm ou GELU/gate apenas onde o profile justificar Sobrecarga de kernels pequenos ou tráfego de ativação Os kernels rápidos existentes já cobrem muito disso; manter a semântica exata do GELU
Recurso de produto separado Deduplicar entradas idênticas de forward Cargas de trabalho que realmente repetem perguntas Relatar contagem de inferências únicas e acertos de cache; não apresentar como ganho de velocidade geral de kernel

Compilar o caminho completo de inferência avaliado

Agent.forward() atualmente constrói arrays, chama self.model e avalia o resultado. Não há um mx.compile envolvendo o modelo ou o bloco. A compilação de forma fixa pode reduzir a construção de grafo em Python e fundir operações compatíveis. O MLX documenta especialização de forma e captura explícita de estado; também alerta que a compilação sem forma (shapeless) não pode preservar com segurança operações Python arbitrárias dependentes de forma. Veja o guia oficial de compilação.

Comece com um callable compilado criado após carregar, converter e avaliar os pesos, usando especialização de forma normal. Mantenha o callable não compilado existente para comparação de paridade e compatibilidade com CPU. Para um modelo de inferência congelado, os pesos podem permanecer capturados para essa instância de modelo; se pesos ou estrutura de módulos mudarem, reconstrua o callable ou capture explicitamente o estado relevante. Não reutilize um closure compilado entre substituições de checkpoint.

Teste um wrapper do modelo inteiro e, se limitações de tracing ou o custo de compilação tornarem isso pouco atraente, compile os blocos do encoder e a cabeça separadamente. O código atual lê x.shape em inteiros Python, faz reshape com valores explícitos de lote/comprimento, cria máscaras arange(length) e indexa marcadores usando um intervalo de linhas derivado da forma. Aplicar shapeless=True a este grafo completo sem redesenho é inseguro. Mover a construção dinâmica de máscaras para fora de um bloco compilado e usar operações de flatten/unflatten independentes de forma pode viabilizar uma variante shapeless mais tarde; verifique-a com B, L e contagens de marcadores alterados.

Buckets de forma podem limitar o retracing, mas o padding tem um custo de computação. Fazer padding de L=93 para 96 adiciona aproximadamente 3.2% de trabalho por token; fazer padding para 128 adiciona aproximadamente 37.6%. Compare a compilação de forma exata com múltiplos pequenos de comprimento e um conjunto limitado de buckets informados pela carga de trabalho. Inclua (batch size, padded length, marker slots, dtype, device/model instance) nas decisões de política de cache e meça a latência de compilação a frio e a memória retida sob churn de formas.

O benchmark forward autônomo em worker.py chama agent.model diretamente. Se a compilação for adicionada apenas em Agent.forward, o benchmark forward atual a ignoraria, enquanto o benchmark ponta a ponta a usaria. Ambos os caminhos devem selecionar explicitamente a mesma implementação candidata para uma comparação significativa. Mantenha mx.eval e a sincronização de GPU no procedimento de medição de tempo: medir apenas a construção de grafo não mediria a inferência.

Agrupe por tokens úteis e depois inspecione o agendamento de matrizes

collate_items preenche cada bloco à direita até sua sequência mais longa; o runtime agrupa as perguntas na ordem de inserção. Para tráfego heterogêneo, ordene ou agrupe por comprimento preparado, use um orçamento de tokens além de um teto de contagem de perguntas e restaure os IDs originais das perguntas e a ordem de saída. Compare lotes de 1, 2, 4, 8, 16, 32 e 64 apenas onde forem representativos do serviço. Para requisições online, inclua o tempo de espera por um lote; perguntas/segundo offline, por si só, podem esconder uma latência inaceitável.

A atual carga de trabalho curta de 50 perguntas desperdiça aproximadamente 8.9% dos tokens com padding para o Laya e 5.8% para o multilíngue. As linhas de benchmark longas não têm desperdício de padding. Consequentemente, remover o padding ou ordenar, sozinhos, tem ganho aritmético limitado nestes fixtures. Uma distribuição de comprimentos mistos, incluindo uma pergunta longa entre muitas curtas, é necessária para revelar o benefício em produção. A remoção do padding deve preservar as posições RoPE por exemplo, as posições dos marcadores e as fronteiras de atenção; concatenar exemplos em uma única sequência sem uma máscara de isolamento muda o modelo.

Para kernels densos, inspecione formas e strides reais. O QKV já é uma única projeção, e os dois ramos de entrada do MLP do encoder já compartilham uma projeção. Dividi-los indiscriminadamente adicionaria lançamentos. Compare o flatten explícito de entrada contígua [B,L,D] em [B*L,D] apenas se o profiler ou o trace de despacho do MLX mostrar GEMMs em lote indesejáveis; o framework pode já fazer o flatten de forma eficiente. A inspeção do código-fonte sozinha não justifica alegar uma otimização de GEMM perdida.

Não insira uma sincronização após cada camada na implementação de produção. O runtime atual avalia uma vez por bloco. Esperas extras poderiam remover a sobreposição CPU/GPU e obscurecer uma melhoria de agendamento; o profiling em nível de camada deve ser uma execução de diagnóstico separada.

Quantização: mire o backbone e implemente seu contrato de armazenamento

A implementação de camadas quantizadas do MLX 0.32.2 instalado fornece nn.quantize(..., class_predicate=...) e QuantizedLinear, com multiplicação de matrizes somente de pesos via mx.quantized_matmul. A quantização afim agrupada é compatível com experimentos de 8 e 4 bits. Comece com camadas lineares do encoder com tamanho de grupo 64, mantendo ativações, norms, embeddings de tipo, scorer e cabeça de ação em FP16. Depois adicione independentemente camadas lineares da cabeça de decisão e, opcionalmente, quantização de embedding. Meça cada variante; kernels somente de pesos podem perder para GEMMs FP16 em contagens altas de tokens.

Há riscos concretos de integração no loader e no modelo atuais:

  1. Agent.__init__ converte todo peso armazenado para um dtype de ponto flutuante e instancia apenas módulos densos antes do carregamento estrito. Um checkpoint quantizado exige metadados descrevendo os módulos selecionados, o tamanho do grupo, a largura em bits e o modo; instancie os módulos quantizados correspondentes antes de carregar e preserve os pesos inteiros empacotados. Uma conversão para ponto flutuante de pesos empacotados não é um carregamento válido.
  2. A primeira camada linear da cabeça de ação tem largura de entrada D+4, ou seja, 1028 ou 772, que não é divisível por um tamanho de grupo afim de 32, 64 ou 128. Uma chamada de quantização geral, portanto, não é adequada. Verifique a largura de entrada de cada camada selecionada antes da conversão.
  3. DecisionModel.__call__ escolhe o dtype de entrada da cabeça de ação a partir de self.act_head.layers[0].weight.dtype. Para uma camada quantizada, esse peso seria armazenamento inteiro empacotado, não o dtype de ativação desejado. Excluir a cabeça de ação evita esse caminho inicialmente; suportá-la depois exige um contrato explícito de dtype de ativação.
  4. A quantização altera logits e probabilidades calibradas. A concordância existente no pequeno fixture FP16 é evidência insuficiente para a qualidade em 4 bits. Use tarefas rotuladas separadas de choice, score e noul, entradas multilíngues, decisões apertadas, diferentes contagens de opções e exemplos de escalonamento. Acompanhe concordância de argmax, precisão na tarefa, erro de score, deriva de probabilidade, calibração e probabilidades de ação. Saídas de ação saturadas podem esconder grandes mudanças nos logits de ação.

Para escalas e offsets afins em FP16 com tamanho de grupo 64, o armazenamento aproximado da matriz é bits/8 + 4/64 bytes por parâmetro: 1.0625 bytes em 8 bits e 0.5625 bytes em 4 bits, comparado com 2 bytes em FP16. Estas são estimativas de armazenamento para matrizes quantizadas, excluindo outros tensores e sobrecarga de empacotamento; não são estimativas de ganho de velocidade. A API oficial de quantização descreve a divisibilidade de grupo e os formatos.

Formatos mais novos de poucos bits devem ser avaliados contra o backend real do M3 Max, e não presumidos como usando hardware de chips Apple posteriores. A verificação de disponibilidade de NAX do MLX exige uma geração de arquitetura mais nova que o dispositivo applegpu_g15s registrado. Veja a verificação de dispositivo do MLX 0.32.2.

Reutilize a preparação na CPU onde as entradas forem realmente idênticas

build_sequence serializa, sanitiza e tokeniza o mesmo state separadamente para cada pergunta. Também tokeniza separadamente cada instrução e opção. O tokenizer Rust já é usado diretamente; substituir a tokenização do Transformers não é uma otimização pendente.

Serialize e sanitize o state uma vez por chamada de prepare, codifique-o uma vez e fatie seus IDs de token para o espaço disponível de cada pergunta. Faça cache de prefixos imutáveis de perguntas preparadas quando a mesma rubrica for usada entre states, com chaves que incluam identidade/revisão do tokenizer, tipo de pergunta, critérios ordenados, serialização de instrução, sanitização de tokens especiais e orçamentos de tokens. Caches limitados não devem reutilizar resultados após mudanças de tokenizer ou configuração. A codificação em lote do tokenizer é outro experimento, desde que sua saída corresponda exatamente à sequência atual de codificações independentes.

Não tokenize um prompt recém-concatenado como substituto de concatenar peças codificadas independentemente: as fronteiras de subpalavra podem mudar. Verifique byte a byte os IDs de entrada, máscaras de atenção, posições de marcadores, qtypes, comportamento de truncamento, critérios estruturados, literais de máscara, entradas vazias e mapeamento de saída.

No benchmark longo, o state repete uma frase 200 vezes antes do truncamento. Evitar N codificações repetidas do state poderia ajudar a preparação na CPU, mas a diferença de tempo existente entre ponta a ponta e forward não mede essa economia. Faça benchmark de prepare, da montagem de collate/arrays, do forward e do pós-processamento independentemente, depois confirme o resultado ponta a ponta com um corpus separado e não repetitivo.

Um cache KV de decoder não se aplica a este encoder. Sua primeira camada é atenção bidirecional global; as representações dos tokens de state dependem da pergunta, das opções e de suas posições. Reutilizar estados ocultos do state ou K/V entre perguntas diferentes altera os resultados. A tokenização e resultados idênticos de entrada inteira podem ser cacheados; estado contextual arbitrário do encoder não pode.

Pode exatamente as saídas finais da cabeça de decisão

Após a última HeadLayer, apenas o token [CLS] e os tokens marcadores de opção são consumidos. As camadas anteriores da cabeça ainda devem produzir todos os tokens, porque a camada final lê seus K/V. Apenas na camada final:

  1. Normalize todos os tokens de entrada e calcule todos os K/V.
  2. Reúna Q em [CLS] e nas posições válidas de opção e rode essas queries contra a sequência completa de K/V mascarada.
  3. Aplique a projeção de saída, o residual, a segunda norm e a rede feed-forward apenas a essas posições selecionadas.
  4. Use a saída [CLS] selecionada para a cabeça de ação e as saídas de marcador selecionadas para a pontuação; preserve o padding de marcadores e a ordem original.

Uma primeira implementação pode reter a projeção QKV completa e fundida e reunir Q depois. Uma variante mais agressiva divide seus pesos em uma projeção KV de comprimento total e uma projeção Q de tokens selecionados. Isso economiza mais aritmética, mas pode tornar o agendamento de GEMM menos eficiente. Índices duplicados de marcadores com padding são inofensivos apenas se os resultados mascarados permanecerem inobserváveis. Os casos de 1 opção, muitas opções e marcadores variáveis precisam de verificações explícitas de paridade. Como menos queries podem selecionar um kernel SDPA diferente, a equivalência matemática não implica resultados de ponto flutuante bit a bit idênticos.

Com R = 1 + number of option slots, reter o QKV completo remove aproximadamente 18 * B * (L-R) * D^2 FLOPs densos e 4 * B * L * (L-R) * D FLOPs de atenção da última camada da cabeça. Dividir Q/KV muda o coeficiente denso de 18 para 20. Em relação à estimativa aritmética do modelo inteiro acima, usar R=5 dá:

Checkpoint / comprimento Manter QKV completo fundido Também calcular apenas Q selecionado
Laya, L=93 2.44% 2.70%
Laya, L=512 2.60% 2.86%
Decisões tipadas, L=1024 2.66% 2.90%
Multilíngue, L=93 4.03% 4.47%
Multilíngue, L=1024 4.22% 4.58%

Estas são reduções estáticas de FLOPs, não reduções previstas de latência. A técnica remove a maior parte do trabalho de uma camada da cabeça, não a maior parte do trabalho do modelo. É útil porque preserva as dependências e é implementável sem retreinamento, não porque prometa um múltiplo da velocidade do modelo inteiro.

Construa atenção genuinamente local só depois de medir sua contribuição

Todas as chamadas de atenção do encoder já usam mx.fast.scaled_dot_product_attention; o RoPE já é mx.fast.rope; nn.LayerNorm chama a primitiva rápida de normalização. A API de atenção do MLX aceita máscaras booleanas e realiza o softmax em FP32. A dimensão atual da cabeça é 64. O despacho Metal do MLX 0.32.2 suporta essa forma com máscaras de array e não seleciona o fallback não fundido para ela durante a inferência. Não há evidência de que a máscara booleana do Laya desative a atenção fundida. force_fused=True, disponível na versão instalada, é útil como asserção de diagnóstico, mas não deve ser anunciado como um novo caminho rápido aqui.

A limitação restante é a esparsidade estruturada. A implementação constrói uma máscara booleana local densa de forma [B,1,L,L]. No kernel Metal de atenção convencional, o laço não causal percorre todo o intervalo de tiles de KV; a máscara de array é aplicada aos scores após a multiplicação QK. Ele preserva a semântica de atenção local sem explorar um intervalo local de tiles.

Um kernel especializado exato pode limitar cada tile de query à janela de K/V sobreposta, manter a acumulação de softmax em FP32 e evitar uma máscara densa L por L. A janela correta é bidirecional e inclusiva: abs(query_position - key_position) <= 64. Queries interiores podem ver 129 posições, apesar do nome de configuração local_attention=128. As camadas de atenção completa e as duas camadas da cabeça de decisão devem permanecer globais. As chaves com padding devem permanecer excluídas, e queries com padding não usadas precisam de comportamento finito definido.

Um protótipo de menor esforço pode agrupar blocos de query com fatias de K/V sobrepostas e chamar o SDPA existente com uma máscara exata menor. Use Q/K RoPE já posicionados, ou retenha explicitamente os offsets absolutos. Prefira blocos em lote em vez de muitas chamadas Python e leve em conta a materialização duplicada de K/V. Este protótipo pode perder para o kernel atual em comprimentos curtos; é um experimento de correção e ponto de equilíbrio antes de manter Metal personalizado.

A redução máxima em FLOPs modelados do modelo inteiro ao remover todos os pares proibidos de atenção local é pequena para entradas curtas e mais promissora para as longas:

Checkpoint / comprimento Redução total ideal de FLOPs com esparsidade local exata
Laya, L=93 0.086%
Laya, L=512 3.61%
Decisões tipadas, L=1024 7.69%
Multilíngue, L=93 0.147%
Multilíngue, L=1024 11.92%

As estimativas usam local_pairs = L*(2*r+1) - r*(r+1) para L>r, com r=64. Elas incluem todas as projeções densas e as duas camadas da cabeça de decisão com atenção completa. Excluem a geração de máscara e o tráfego de memória. O benefício em runtime pode superar ou ficar abaixo da fração de FLOPs porque atenção e GEMMs têm eficiências diferentes; só o profiling pode estabelecê-lo. Em 8192 tokens, o trade-off seria diferente, mas os agents fornecidos limitam as entradas a 512 ou 1024, então uma alegação de 8192 tokens exigiria uma carga de trabalho suportada separadamente.

Fusão além da compilação

O nn.gelu exato instalado já está decorado com compilação shapeless, e nn.Linear já usa um addmm ciente de bias quando apropriado. A compilação do bloco inteiro ainda pode fundir o GELU com sua multiplicação de gate, adições residuais, casts, máscaras e pequenas operações de feature de score. Inspecione o grafo de kernel compilado antes de implementar um kernel personalizado equivalente.

Se o tráfego de ativação continuar significativo, prototipe a fusão exata de GELU-e-gate ou de residual-e-LayerNorm. Preserve o GELU exato atual, baseado em erf; uma aproximação por tanh ou sigmoid muda o modelo e precisa de medições de qualidade separadas. Inspecione strides e cópias reais de Q/K/V antes de adicionar conversões de layout: a implementação de atenção completa do MLX aceita uma dimensão de cabeça contígua com outros strides e escreve um layout de saída conveniente para mesclar cabeças. Uma cópia contígua incondicional pode adicionar trabalho.

O softmax de marcadores, a ordenação dos dois maiores, a entropia, a pequena cabeça de ação e a formatação do resultado em NumPy são alvos legítimos mais tarde, mas só se forem medidos. Há poucas posições de opção em comparação com centenas de operações de transformer de largura total, então otimizá-las primeiro provavelmente não aborda o caminho dominante.

Proteja o significado do benchmark ao buscar ganhos agressivos

O gerador de carga de trabalho atual cicla três definições de pergunta para construir 5, 10 ou 50 perguntas. Há no máximo três entradas únicas de modelo nesses lotes. A deduplicação exata por chamada pode evitar inferência redundante em uma aplicação real, mas melhoraria desproporcionalmente esses fixtures. Mantenha este recurso separado da otimização de kernel e relate questions, unique_forward_inputs, acertos de cache e tokens realmente avaliados. Reconstrua cada resposta original usando seus próprios rótulos, critérios ordenados e metadados de calibração. Mantenha uma suíte com 50 perguntas genuinamente diferentes e uma suíte com entradas deliberadamente duplicadas.

Não use cache de resultados entre chamadas para o benchmark de inferência principal: ele chama repetidamente exatamente a mesma requisição. Relate qualquer experimento de cache como tal. Benchmarks de cache de tokenizer devem incluir tanto um cenário de rubrica repetida quanto um cenário de entrada nova.

Para cada candidato, use este desenho de experimento:

  1. Mantenha o alvo constante. Registre revisão/hash da origem, revisão do modelo, dtype, flags de compilação, módulos quantizados selecionados, IDs de token ou seu hash, forma, contagem de marcadores, política de batching, warmup, sincronização e dispositivo. O input_sha256 existente faz hash de state/questions, e não de tensores de token reais; ele não estabelece identidade de tensor entre tokenizers. Reexecute a linha de base a partir da mesma revisão final de origem, porque hashes históricos de origem no JSON diferem.
  2. Separe as classes de carga de trabalho. Use formas fixas para comparações controladas de kernel; comprimentos variáveis reais para agendamento e compilação; perguntas distintas para vazão; rubricas repetidas para cache legítimo na CPU; e perguntas deliberadamente duplicadas para deduplicação. Inclua caudas de comprimentos longos e diferentes contagens de opções. Mantenha o texto de origem e o truncamento idênticos dentro de cada comparação de backend.
  3. Meça o comportamento a frio e a quente. Registre o carregamento do modelo e a primeira compilação separadamente. Meça preparação, construção de grafo, forward sincronizado e avaliado, conversão de saída e latência ponta a ponta sem tratar medianas independentes como aditivas. Faça profiling de kernels em uma execução separada, porque o tracing pode perturbar a latência.
  4. Mude uma otimização por vez. Faça a triagem com a contagem de iterações existente, depois repita os finalistas em blocos alternados de linha de base/candidato e colete amostras suficientes para um p95 crível, por exemplo pelo menos 200 requisições cronometradas por carga de trabalho. Use um único benchmark ativo de GPU, condições de energia/térmicas consistentes e preserve as amostras brutas. Exija uma melhoria maior que a variabilidade medida entre execuções.
  5. Verifique correção e estabilidade. Compare com o MLX no mesmo dtype e com a referência FP32 existente; imponha identidade de tokens para mudanças de agendamento e preparação na CPU. Exercite comprimentos em torno de 64, 128 e fronteiras de bucket; lotes em torno dos limites de chunk; uma e muitas opções; entradas multilíngues; padding; e mudanças de forma após a compilação. Repita requisições com forma variável, acompanhe tanto a memória ativa quanto a de cache após o warmup e verifique resultados determinísticos finitos dentro de cada configuração.
  6. Aplique portões mais fortes a mudanças aproximadas. Quantização, aproximação de ativação, poda de tokens, saídas antecipadas e destilação exigem resultados de tarefa e calibração separados, além do pequeno fixture de regressão. Preserve uma identidade de modelo e um rótulo de benchmark separados ao mudar o comportamento treinado. Um checkpoint multilíngue mais rápido ou um modelo destilado é um modelo diferente, não um ganho de velocidade do checkpoint Laya idêntico.

Para qualquer hotspot medido ocupando a fração f do tempo ponta a ponta e acelerado por um fator s, use o limite de Amdahl 1 / (1 - f + f/s) para avaliar o impacto total esperado. Use uma fração de tempo medida para f; as frações aritméticas acima não são substitutas. A próxima decisão concreta de engenharia deve seguir a ablação de compilação/batching e um perfil de kernel curto versus longo, e não um multiplicador não verificado.

Notas de documentação e reprodutibilidade

A consulta à documentação usou o fluxo de trabalho exigido do Context7: uma resolução library MLX e depois consultas separadas à documentação oficial para compilação e atenção rápida, usando /websites/ml-explore_github_io_mlx_build_html (três comandos no total). Fontes .pyi instaladas e códigos-fonte Python foram inspecionados para verificar o comportamento do MLX 0.32.2, incluindo force_fused, APIs de quantização, GELU compilado e LayerNorm rápida. Fontes C++/Metal do upstream fixadas por versão foram lidas para detalhes de despacho e de laço de tiles. Nenhuma biblioteca foi atualizada para esta pesquisa.

Os dois arquivos de linha de base FP16 usados para as observações detalhadas tinham estes resumos SHA-256 no momento da inspeção:

laya-mlx-float16.json
63146dd664d039dde1a728b17aad896e491bd01ea36ea0786953691180e55b09

laya-multilingual-mlx-float16.json
9af74bd5a11e4edc15e6a8c9dc929a7b9fd2d19cb06f348cd0e04a076f912473

O processo principal de benchmark ainda estava produzindo artefatos adicionais durante esta pesquisa. A tabela cita deliberadamente arquivos de linha de base completos já disponíveis no momento da revisão e não faz alegações sobre implementações candidatas não medidas.