Documentação

Investigação de desempenho do Laya MLX

Data da investigação: 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 MLX instalada, da documentação oficial e do JSON de benchmark existente. Não foi corrido nenhum benchmark de GPU nem inferência de modelo para esta investigação. Nenhuma das otimizações propostas abaixo tem um aumento de velocidade medido neste relatório.

As primeiras experiências devem ser a compilação do modelo completo e o escalonamento representativo de lotes, seguidas da multiplicação de matrizes quantizada seletiva. Estas abordam o trabalho repetido dominante. Um kernel especializado de atenção local é um projeto credível a mais longo prazo para entradas longas. A poda exata da última camada da cabeça de decisão é viável, mas a sua poupança aritmética no modelo completo é de apenas alguns por cento. Grandes melhorias sem alterar o checkpoint exigirão melhorar a espinha dorsal densa, eliminar pedidos genuinamente redundantes ou encontrar um estrangulamento medido da implementação; simplesmente substituir uma ativação ou ativar outro flag de atenção dificilmente será suficiente.

O que as medições existentes estabelecem

Os seguintes são tempos de latência medianos ponta a ponta existentes, incluindo a preparação do prompt e a formatação do resultado, com conclusão sincronizada da GPU, cinco warmups e 50 iterações medidas. O carregamento e os downloads do modelo estão excluídos. O benchmark permite um lote de 64 perguntas; o padrão do runtime público é 16, por isso o resultado de 50 perguntas não é a configuração predefinida da API.

Checkpoint / precisão 1 pergunta curta 10 perguntas curtas 50 perguntas curtas 1 pergunta longa 10 perguntas longas
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 stock Torch MPS FP32 24.918 ms 95.265 ms 497.856 ms 65.581 ms 586.594 ms
Multilingual MLX FP16 7.390 ms 27.386 ms 127.565 ms 37.635 ms 389.487 ms
Multilingual MLX FP32 7.988 ms 32.337 ms 151.387 ms 47.208 ms 451.331 ms
Multilingual stock Torch MPS FP32 19.349 ms 43.158 ms 194.171 ms 52.939 ms 534.492 ms

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

Há uma variabilidade significativa entre execuções. Por exemplo, a execução multilingue FP16 de 10 perguntas longas tem p50 389.487 ms, p95 462.319 ms e máximo 619.663 ms. A sua mediana de passagem direta curta de pergunta única é 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 ficheiros atuais não isolam o tokenizer, o despacho Python, os kernels individuais da GPU nem os custos de sincronização. Estabelecem bases úteis, não um diagnóstico de estrangulamento ao nível do kernel.

Os relatórios de validação registam uma concordância de argmax de 63/63 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. Estes são testes de regressão sobre um pequeno corpus de fixtures, incluindo perguntas repetidas. Não são prova de que futuras mudanças de quantização ou de arquitetura preservem a exatidão geral das tarefas.

Porque a multiplicação de matrizes densa merece prioridade

A implementação do modelo aplica projeções QKV e de saída, uma MLP de codificador com gating e duas camadas convencionais de Transformer da cabeça de decisão a cada token com padding. Seja D o tamanho oculto, I o tamanho intermédio do codificador, N o número de camadas do codificador e H o número de camadas da cabeça de decisão. O número de pesos de matriz usados por token nestes 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

Estas estimativas contam a multiplicação e a 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 checkpoints D / I / camadas do codificador Pesos de embedding de tokens Principais pesos de matrizes por token A Camadas globais / locais do codificador
Laya / typed decisions 1024 / 2624 / 28 51,576,832 368,312,320 10 / 18
Multilingual 768 / 1152 / 22 196,608,000 124,452,864 8 / 14

Os aproximadamente 322 milhões de parâmetros totais do checkpoint multilingue incluem 196.6 milhões de parâmetros de embedding. Apenas linhas de embedding selecionadas são reunidas para a inferência; isto não é uma projeção de vocabulário completa. A sua principal carga de trabalho de matrizes por token é cerca de um terço da do modelo em inglês, apesar de as 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 estrangulamento de hardware específico. A quantização apenas dos embeddings reduziria predominantemente o tamanho dos pesos residentes, especialmente para o multilingue; não tem necessariamente de 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 multilingue. A sua fração do tempo real pode diferir substancialmente. Um profiler deve distinguir kernels de matrizes, kernels de atenção, kernels elementwise, construção de grafos na CPU e intervalos de inatividade antes de se comprometer com trabalho Metal personalizado.

Experiências priorizadas

Prioridade Experiência Melhor alvo Principal compromisso / condição de aceitação
P0 Fazer o perfil de uma forma curta e uma longa; compilar o modelo ou os blocos do codificador Latência de um único pedido e despacho Python Manter saídas idênticas dentro da tolerância de precisão existente; medir a compilação de primeiro uso separadamente
P0 Afinar o batching pelo orçamento real de tokens e pela distribuição de comprimentos Muitas perguntas distintas e tráfego de comprimentos mistos Otimizar o débito sujeito a um orçamento de latência p95 e de memória; ter em conta a fila de espera
P1 Quantizar camadas lineares selecionadas da espinha dorsal, 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 do estado partilhado e de modelos de pergunta estáveis Muitas perguntas que partilham um estado ou rubricas repetidas Identidade exata dos tokens; cache limitada; separar resultados com e sem cache
P1 Calcular apenas as saídas necessárias da camada final da cabeça Todas as cargas de trabalho, especialmente sequências mais longas Poda exata de dependências; poupança aritmética modesta no modelo completo
P2 Atenção real de janela local com limites de tile Cargas de trabalho de 512/1024 tokens Nova complexidade de kernel; preservar a janela bidirecional e a semântica de padding
P2 Fundir residual/norm ou GELU/gate apenas onde o perfil o justifique Sobrecarga de kernels pequenos ou tráfego de ativações Os kernels rápidos existentes já cobrem grande parte disto; manter a semântica exata do GELU
Funcionalidade de produto à parte Eliminar duplicados de entradas idênticas de passagem direta Cargas de trabalho que repetem realmente perguntas Reportar a contagem de inferências únicas e os acertos de cache; não o apresentar como um aumento de velocidade geral de kernel

Compilar o caminho de inferência avaliado completo

Agent.forward() atualmente constrói arrays, chama self.model e avalia o resultado. Não há nenhum mx.compile envolvente ao nível do modelo ou do bloco. A compilação de forma fixa pode reduzir a construção de grafos em Python e fundir operações suportadas. O MLX documenta a especialização de forma e a captura explícita de estado; também avisa que a compilação sem forma (shapeless) não consegue preservar com segurança operações Python arbitrárias dependentes da forma. Vê o guia oficial de compilação.

Começa com um callable compilado criado depois de carregar, converter e avaliar os pesos, usando especialização de forma normal. Mantém o callable não compilado existente para comparação de paridade e compatibilidade com a CPU. Para um modelo de inferência congelado, os pesos podem permanecer capturados para essa instância do modelo; se os pesos ou a estrutura dos módulos mudarem, reconstrói o callable ou captura explicitamente o estado relevante. Não reutilizes um fecho compilado entre substituições de checkpoint.

Testa um wrapper do modelo completo e, se as limitações de tracing ou o custo de compilação o tornarem pouco atrativo, compila os blocos do codificador e a cabeça separadamente. O código atual lê x.shape para inteiros Python, faz reshape com valores explícitos de batch/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 flatten/unflatten independentes da forma pode permitir uma variante shapeless posterior; verifica-a em B, L e contagens de marcadores alterados.

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

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 iria contorná-la, enquanto o benchmark ponta a ponta a usaria. Ambos os caminhos devem selecionar explicitamente a mesma implementação candidata para uma comparação significativa. Mantém mx.eval e a sincronização da GPU no procedimento de medição de tempo: medir apenas a construção do grafo não mediria a inferência.

Agrupar por tokens úteis, depois inspecionar o escalonamento de matrizes

collate_items faz padding à direita de cada bloco até à sua sequência mais longa; o runtime agrupa as perguntas pela ordem de inserção. Para tráfego heterogéneo, ordena ou agrupa por comprimento preparado, usa um orçamento de tokens além de um limite de contagem de perguntas, e restaura os IDs originais das perguntas e a ordem de saída. Compara lotes de 1, 2, 4, 8, 16, 32 e 64 apenas onde forem representativos do serviço. Para pedidos online, inclui 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 de 50 perguntas curtas desperdiça aproximadamente 8.9% dos tokens com padding para o Laya e 5.8% para o multilingue. As linhas longas do benchmark não têm desperdício de padding. Por conseguinte, remover o padding ou ordenar sozinho tem um ganho aritmético limitado nestes fixtures. É necessária uma distribuição de comprimentos mistos, incluindo uma pergunta longa entre muitas curtas, para revelar o benefício em produção. A remoção do padding deve preservar as posições RoPE por exemplo, as posições de marcadores e os limites de atenção; concatenar exemplos numa única sequência sem uma máscara de isolamento altera o modelo.

Para kernels densos, inspeciona as formas e os strides reais. O QKV já é uma única projeção, e os dois ramos de entrada da MLP do codificador já partilham uma projeção. Dividi-los indiscriminadamente acrescentaria lançamentos. Compara o flatten explícito da entrada contígua [B,L,D] para [B*L,D] apenas se o profiler ou o traço de despacho do MLX mostrar GEMMs em lote indesejáveis; a framework pode já fazer flatten de forma eficiente. A inspeção do código-fonte por si só não justifica reivindicar uma otimização de GEMM perdida.

Não insiras uma sincronização após cada camada na implementação de produção. O runtime atual avalia uma vez por bloco. Esperas extra poderiam eliminar a sobreposição CPU/GPU e obscurecer uma melhoria de escalonamento; a criação de perfis ao nível da camada deve ser uma execução de diagnóstico separada.

Quantização: visar a espinha dorsal e implementar o seu contrato de armazenamento

A implementação de camadas quantizadas do MLX 0.32.2 instalada fornece nn.quantize(..., class_predicate=...) e QuantizedLinear, com multiplicação de matrizes apenas de pesos através de mx.quantized_matmul. A quantização afim agrupada suporta experiências de 8 bits e 4 bits. Começa com camadas lineares do codificador com tamanho de grupo 64, mantendo ativações, norms, embeddings de tipo, scorer e cabeça de ação em FP16. Depois acrescenta independentemente camadas lineares da cabeça de decisão e, opcionalmente, a quantização de embeddings. Mede cada variante; os kernels apenas de pesos podem perder para GEMMs FP16 em contagens elevadas de tokens.

Há perigos concretos de integração no carregador e no modelo atuais:

  1. Agent.__init__ converte todos os pesos armazenados para um dtype de vírgula flutuante e instancia apenas módulos densos antes do carregamento estrito. Um checkpoint quantizado requer metadados que descrevam os módulos selecionados, o tamanho do grupo, a largura em bits e o modo; instancia os módulos quantizados correspondentes antes de carregar e preserva os pesos inteiros empacotados. Uma conversão para vírgula 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 generalizada é, por isso, inadequada. Verifica 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, e não o dtype de ativação pretendido. Excluir a cabeça de ação evita inicialmente este caminho; suportá-la mais tarde requer um contrato explícito de dtype de ativação.
  4. A quantização altera os logits e as probabilidades calibradas. A concordância existente nos pequenos fixtures FP16 é prova insuficiente para a qualidade de 4 bits. Usa tarefas choice, score e noul rotuladas e reservadas, entradas multilingues, decisões próximas, diferentes contagens de opções e exemplos de escalonamento. Acompanha a concordância de argmax, a exatidão das tarefas, o erro de pontuação, a deriva de probabilidade, a calibração e as probabilidades de ação. Saídas de ação saturadas podem esconder grandes mudanças nos logits de ação.

Para escalas e desvios afins 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, em comparação com 2 bytes em FP16. Estas são estimativas de armazenamento para matrizes quantizadas, excluindo outros tensores e a sobrecarga de empacotamento; não são estimativas de aumento de velocidade. A API oficial de quantização descreve a divisibilidade de grupos e os formatos.

Os formatos mais recentes de baixos bits devem ser avaliados contra o backend real do M3 Max, e não se deve assumir que usam hardware de chips Apple posteriores. A verificação de disponibilidade do NAX do MLX exige uma geração de arquitetura mais recente do que o dispositivo applegpu_g15s registado. Vê a verificação de dispositivo do MLX 0.32.2.

Reutilizar a preparação na CPU onde as entradas são realmente idênticas

build_sequence serializa, sanitiza e tokeniza o mesmo estado 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.

Serializa e sanitiza o estado uma vez por chamada a prepare, codifica-o uma vez e fatia os seus IDs de token para o espaço disponível de cada pergunta. Faz cache de prefixos de perguntas preparados e imutáveis quando a mesma rubrica é usada entre estados, com chaves que incluam a identidade/revisão do tokenizer, o tipo de pergunta, os critérios ordenados, a serialização das instruções, o saneamento de tokens especiais e os orçamentos de tokens. As caches limitadas não devem reutilizar resultados após alterações no tokenizer ou na configuração. A codificação em lote do tokenizer é outra experiência, desde que a sua saída corresponda exatamente à atual sequência de codificações independentes.

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

Para o benchmark longo, o estado repete uma frase 200 vezes antes da truncagem. Evitar N codificações repetidas do estado poderia ajudar a preparação na CPU, mas a diferença de tempo ponta a ponta/forward existente não mede essa poupança. Mede separadamente prepare, a construção de collate/array, o forward e o pós-processamento, e depois confirma o resultado ponta a ponta com um corpus reservado que não se repete.

Uma cache KV de descodificador não se aplica a este codificador. A sua primeira camada é atenção bidirecional global; as representações dos tokens do estado dependem da pergunta, das opções e das suas posições. Reutilizar estados ocultos do estado ou K/V entre perguntas diferentes altera os resultados. A tokenização e resultados idênticos de entrada completa podem ser colocados em cache; estados contextuais arbitrários do codificador não podem.

Podar 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 têm de continuar a produzir todos os tokens, porque a camada final lê os seus K/V. Apenas na camada final:

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

Uma primeira implementação pode manter a projeção QKV completa fundida e reunir o Q depois. Uma variante mais agressiva divide os seus pesos numa projeção KV de comprimento total e numa projeção Q dos tokens selecionados. Isto poupa mais aritmética, mas pode tornar o escalonamento de GEMM menos eficiente. Os índices de marcador duplicados 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 vírgula flutuante idênticos bit a bit.

Com R = 1 + number of option slots, manter 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 altera o coeficiente denso de 18 para 20. Em relação à estimativa aritmética do modelo completo acima, usando R=5 obtém-se:

Checkpoint / comprimento Manter o QKV completo fundido Calcular também apenas o Q selecionado
Laya, L=93 2.44% 2.70%
Laya, L=512 2.60% 2.86%
Typed decisions, L=1024 2.66% 2.90%
Multilingual, L=93 4.03% 4.47%
Multilingual, L=1024 4.22% 4.58%

Estas são reduções estáticas de FLOPs, não reduções de latência previstas. 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 re-treino, não porque prometa um múltiplo de velocidade do modelo completo.

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

Todas as chamadas de atenção do codificador já usam mx.fast.scaled_dot_product_attention; o RoPE já é mx.fast.rope; nn.LayerNorm chama a primitiva de normalização rápida. 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 esta forma com máscaras de array e não seleciona o fallback não fundido para ela durante a inferência. Não há provas de que a máscara booleana do Laya desative a atenção fundida. O force_fused=True, disponível na versão instalada, é útil como asserção de diagnóstico, mas não deve ser publicitado aqui como um novo caminho rápido.

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 de atenção Metal convencional, o ciclo não causal percorre todo o intervalo de tiles KV; a máscara de array é aplicada às pontuações após a multiplicação QK. Preserva a semântica de atenção local sem explorar um intervalo de tiles local.

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

Um protótipo de menor esforço pode agrupar blocos de query com fatias K/V sobrepostas e chamar o SDPA existente com uma máscara exata mais pequena. Usa Q/K com RoPE já posicionado, ou retém explicitamente os deslocamentos absolutos. Prefere blocos em lote a muitas chamadas Python, e tem em conta a materialização duplicada de K/V. Este protótipo pode perder para o kernel atual em comprimentos curtos; é uma experiência de correção e de ponto de equilíbrio antes de manter Metal personalizado.

A redução máxima nos FLOPs modelados do modelo completo ao remover todos os pares de atenção local proibidos é 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%
Typed decisions, L=1024 7.69%
Multilingual, L=93 0.147%
Multilingual, L=1024 11.92%

As estimativas usam local_pairs = L*(2*r+1) - r*(r+1) para L>r, com r=64. Incluem todas as projeções densas e ambas as camadas de atenção completa da cabeça de decisão. Excluem a geração de máscaras e o tráfego de memória. O benefício em runtime pode exceder ou ficar abaixo da fração de FLOPs, porque a atenção e os GEMMs têm eficiências diferentes; só a criação de perfis o pode estabelecer. A 8192 tokens o compromisso seria diferente, mas os agents fornecidos limitam as entradas a 512 ou 1024, por isso uma afirmação de 8192 tokens exigiria uma carga de trabalho suportada separadamente.

Fusão para além da compilação

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

Se o tráfego de ativações continuar a ser significativo, faz um protótipo de fusão exata GELU-e-gate ou residual-e-LayerNorm. Preserva o GELU exato atual baseado em erf; uma aproximação tanh ou sigmoid altera o modelo e precisa de medições de qualidade separadas. Inspeciona os strides e as cópias reais de Q/K/V antes de acrescentar conversões de layout: a implementação de atenção completa do MLX aceita uma dimensão de cabeça contígua com outro striding e escreve um layout de saída conveniente para fundir cabeças. Uma cópia contígua incondicional pode acrescentar trabalho.

O softmax dos marcadores, a ordenação dos dois primeiros, a entropia, a pequena cabeça de ação e a formatação de resultados com NumPy são alvos legítimos mais tardios apenas se medidos. Há poucas posições de opção em comparação com centenas de operações Transformer de largura completa, por isso otimizá-las primeiro dificilmente abordará o caminho dominante.

Proteger o significado do benchmark ao procurar 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 de modelo únicas nesses lotes. A deduplicação exata por chamada pode evitar inferência redundante numa aplicação real, mas melhoraria desproporcionadamente estes fixtures. Mantém esta funcionalidade separada da otimização de kernels e reporta questions, unique_forward_inputs, os acertos de cache e os tokens realmente avaliados. Reconstrói cada resposta original usando as suas próprias etiquetas, critérios ordenados e metadados de calibração. Mantém uma suite com 50 perguntas genuinamente diferentes e uma suite com entradas deliberadamente duplicadas.

Não uses cache de resultados entre chamadas para o benchmark principal de inferência: ele chama repetidamente exatamente o mesmo pedido. Reporta qualquer experiência de cache como tal. Os benchmarks de cache do tokenizer devem incluir tanto um cenário de rubrica repetida como um cenário de entrada nova.

Para cada candidato, usa este desenho de experiência:

  1. Mantém o alvo constante. Regista a revisão/hash da origem, a revisão do modelo, o dtype, os flags de compilação, os módulos quantizados selecionados, os IDs de token ou o seu hash, a forma, a contagem de marcadores, a política de batching, o warmup, a sincronização e o dispositivo. O input_sha256 existente faz o hash do estado/perguntas em vez dos tensores de token reais; não estabelece a identidade de tensores entre tokenizers. Volta a correr a linha de base a partir da mesma revisão final da origem, porque os hashes de origem históricos dos JSON diferem.
  2. Separa as classes de carga de trabalho. Usa formas fixas para comparações controladas de kernels; comprimentos variáveis reais para escalonamento e compilação; perguntas distintas para o débito; rubricas repetidas para cache legítima na CPU; e perguntas deliberadamente duplicadas para deduplicação. Inclui caudas de comprimento longo e diferentes contagens de opções. Mantém o texto de origem e a truncagem idênticos dentro de cada comparação de backend.
  3. Mede o comportamento a frio e a quente. Regista o carregamento do modelo e a primeira compilação separadamente. Mede a preparação, a construção do grafo, o forward sincronizado e avaliado, a conversão da saída e a latência ponta a ponta, sem tratar medianas independentes como aditivas. Faz o perfil dos kernels numa execução separada, porque o tracing pode perturbar a latência.
  4. Muda uma otimização de cada vez. Faz a triagem com a contagem de iterações existente, depois repete os finalistas em blocos alternados de linha de base/candidato e recolhe amostras suficientes para um p95 credível, por exemplo pelo menos 200 pedidos cronometrados por carga de trabalho. Usa um único benchmark de GPU ativo, condições de energia/térmicas consistentes, e preserva as amostras em bruto. Exige uma melhoria maior do que a variabilidade medida entre execuções.
  5. Verifica correção e estabilidade. Compara com o MLX no mesmo dtype e com a referência FP32 existente; impõe a identidade dos tokens para alterações de escalonamento e de preparação na CPU. Exercita comprimentos em torno de 64, 128 e limites de bucket; lotes em torno dos limites de bloco; uma e muitas opções; entradas multilingues; padding; e mudanças de forma após a compilação. Repete pedidos com forma variável, observa tanto a memória ativa como a de cache após o warmup, e verifica resultados finitos e determinísticos dentro de cada configuração.
  6. Aplica portões mais fortes às mudanças aproximadas. A quantização, a aproximação de ativações, a poda de tokens, as saídas antecipadas e a destilação requerem resultados de tarefas e de calibração reservados, para além do pequeno fixture de regressão. Preserva uma identidade de modelo e um rótulo de benchmark separados ao alterar o comportamento treinado. Um checkpoint multilingue mais rápido ou um modelo destilado é um modelo diferente, não um aumento de velocidade do mesmo checkpoint Laya.

Para qualquer hotspot medido que ocupe a fração f do tempo ponta a ponta e seja acelerado por um fator s, usa o limite de Amdahl 1 / (1 - f + f/s) para avaliar o impacto total esperado. Usa uma fração de tempo medida para f; as frações aritméticas acima não são substitutos. 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, não um multiplicador não verificado.

Notas de documentação e reprodutibilidade

A consulta da documentação usou o fluxo de trabalho Context7 exigido: uma resolução library MLX, 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). Foram inspecionados os .pyi instalados e os códigos-fonte Python para verificar o comportamento do MLX 0.32.2, incluindo force_fused, as APIs de quantização, o GELU compilado e o LayerNorm rápido. Foram lidos os códigos-fonte C++/Metal upstream fixados à versão para obter detalhes de despacho e do ciclo de tiles. Nenhuma biblioteca foi atualizada para esta investigação.

Os dois ficheiros de referência 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 do benchmark ainda estava a produzir artefactos adicionais durante esta investigação. A tabela cita deliberadamente ficheiros de referência completos já disponíveis quando foram revistos e não faz afirmações sobre implementações candidatas não medidas.