Inferência pura de aprendizado de máquina sem servidor com o AWS Lambda e Layers

Klaus Seiler Blocked Desbloquear Seguir Seguindo 9 de janeiro

Depois de projetar e aprender um modelo ML, a parte mais difícil é executá-lo e mantê-lo em produção. A AWS está oferecendo para hospedar e implantar modelos por meio do ML Hosting sob demanda em vários tipos e tamanhos de instâncias, empacotados como o serviço SageMaker. Estes começam com um ml.t2.medium e vão até GPU acelerado ml.p3.16xlarge instâncias.

Para ficar sem servidor em uma implantação sem servidor e também para manter os custos baixos e permitir o escalonamento flexível, é desejável uma inferência pura de modelo de ML baseado em AWS Lambda. Essas abordagens existem e já foram descritas em outro lugar, mas possuem algumas advertências, como a execução por meio de uma interface REST e um ponto de extremidade do Gateway de API. Em uma implementação maior de produção de funções Lambda e step, é possível invocar apenas um fluxo desencadeado de evento Lambda puro sem o desvio de gateway REST e API. Uma vez que muitas ferramentas de IA e ML são baseadas em Python e usam ferramentas baseadas em Python como scikit-learn , um tempo de execução Lambda baseado em Python é escolhido.

No final deste post, é apresentada uma comparação de custos entre essa abordagem sem servidor e a abordagem de hospedagem tradicional.

Este diagrama mostra um fluxo ML específico em um pipeline de processamento de dados maior. Os dados e documentos são decompostos e gravados em uma tabela do DynamoDB e as alterações são colocadas em um fluxo de dados do DynamoDB para vários consumidores de recebimento de dados. Na realidade, é um pouco mais complexo, devido a limitações gerais de processamento e um padrão de fan-out é usado, detalhes mais genéricos são descritos nesta postagem no blog da AWS “Como executar a replicação de dados ordenada entre aplicativos usando Streams do Amazon DynamoDB” .

Índice

Gere e salve o modelo ML

O modelo foi criado via notebooks Jupyter e utiliza o scikit. Como os dados de entrada são baseados em texto, foram utilizadas técnicas de NLP (Natural Language Processing). Para salvar e manter o modelo treinado, o scikit oferece (de) serialização de modelos contendo grandes matrizes de dados através da função Joblib .

 de joblib de importação sklearn.externals 
 # Salvar no arquivo no diretório de trabalho atual 
joblib_file = “MyModel20190104v1.joblib”
joblib.dump (model, joblib_file)
 # Carregar do arquivo 
joblib_model = joblib.load (joblib_file)

Usando modelos salvos de tal maneira requer atenção aos seguintes pontos:

  • A versão do Python subjacente . A mesma versão principal do Python deve ser usada para desserializar o modelo como foi usado para salvar / serializá-lo.
  • Versões da Biblioteca . As versões de todas as principais bibliotecas usadas durante a geração do modelo devem ser as mesmas ao desserializar um modelo salvo. Isso se aplica principalmente à versão do NumPy e à versão do scikit-learn. Além disso, se um SO diferente for usado para construir o modelo, é importante que os programas C compilados nativamente sejam adequados para o tempo de execução do AWS Lambda.

Construa a função de inferência de ML Lambda

Atendendo aos requisitos acima, a função ML Lambda pode ser construída e implementada. Para permitir um fácil gerenciamento do ciclo de vida do modelo subjacente e atualizações fáceis quando o modelo é otimizado ou adaptado aos novos conjuntos de dados, o AWS Lambda Layers, introduzido no re: Invent 2018, será usado.

Camadas do AWS Lambda

Com o AWS Lambda Layers, é possível separar partes e dependências diferentes do código e, portanto, não é necessário carregar todos os artefatos e dependências em cada alteração.

Agora também é possível criar um tempo de execução totalmente customizado que suporte qualquer linguagem de programação, que pode ser o próximo passo para a otimização do tempo de execução do lambda de inferência do ML (consulte a seção posterior a respeito dos tempos de chamada).

Camada 1: crie e publique a camada Dependências com a janela de encaixe

Supõe-se que um ambiente virtual já tenha sido criado (nesse caso com o pipenv) com a versão correta do Python (o ambiente no qual o ajuste e o design do modelo foi feito). O arquivo de requisitos anexo pode ser criado com:

 bloqueio de pipenv -r> requirements.txt 

Para ter uma determinada versão do Python e também o tempo de execução do AWS lambda correspondente, as dependências são criadas por meio de um contêiner docker do projeto LambCI .

O seguinte script get-layer-package.sh pode ser executado para gerar as dependências:

 #! / bin / bash 

exportar PKG_DIR = "python"

rm -rf $ {PKG_DIR} && mkdir -p $ {PKG_DIR}

execução do docker --rm -v $ (pwd) : / foo -w / foo lambci / lambda: build-python3.6
instalação pip -r requirements.txt --no-deps -t $ {PKG_DIR}

Todas as dependências serão exportadas para a pasta Python no ambiente virtual. A estrutura do diretório e os arquivos precisam ser compactados para estarem prontos para upload:

 zip -r MyMLfunction_layer1.zip python 

O pacote zip gerado pode ser enviado diretamente se o tamanho não exceder 50MB, o que é mais provável neste caso. Além disso, há um limite em relação ao tamanho total de uma camada, que é de cerca de 250MB. Portanto, primeiro fazemos um upload para o S3:

 aws s3 cp MyMLfunction_layer1.zip s3: // meubucket / camadas / 

E então publique imediatamente a camada:

 aws lambda versão da camada de publicação --layer-name MyMLfunction_layer1 --content S3Bucket = mybucket, S3Key = camadas / MyMLfunction_layer1.zip --compatible-runtimes python3.6 

Camada 2: publicar o modelo e a camada de pré-processamento

Para o nosso modelo específico, temos dois pré-processadores e o modelo salvo no ambiente virtual, todos com a extensão de arquivo .joblib.

 $ ls * .joblib 
 PreProc1_MyModel.joblib 
PreProc2_MyModel.joblib
MyModel.joblib

Esses arquivos precisam ser compactados e, em seguida, publicados como a camada adicional. Aqui fazemos um upload direto, já que a camada tem apenas alguns MB de tamanho:

 zip MyMLfunction_layer2_model20190104v1.zip * .joblib 
 aws lambda publicar-camada-versão --layer-name MyMLfunction_layer2 - arquivo -zip fileb: //MyMLfunction_layer2_model20190104v1.zip --compatible-runtimes python3.6 

É muito importante para o carregamento posterior no runtime do Lambda, que esses arquivos estejam disponíveis no diretório montado / opt no contêiner do Lambda.

Ter essa camada adicional contendo apenas o modelo nos permite publicar facilmente uma nova versão do modelo mais tarde.

Camada de função: gerar e publicar a função

A função lambda deve ler os dados do fluxo do Kinesis e, em seguida, executar uma previsão usando o modelo carregado. É acionado através dos eventos do Kinesis e do tamanho do lote definido (definido na configuração do lambda para o evento Kinesis). O abaixo lambda_function.py lê todos os dados de texto relevantes e gera uma lista que é então enviada para a função de previsão de uma só vez.

 importar base64 
importar json
de joblib de importação sklearn.externals
 PREP1_FILE_NAME = '/opt/PreProc1_MyModel.joblib' 
PREP2_FILE_NAME = '/opt/PreProc2_MyModel.joblib'
MODEL_FILE_NAME = '/opt/MyModel.joblib'
 def prever (dados): 
# Carregar os pré-processadores do modelo
pre1 = joblib.load (PREP1_FILE_NAME)
pre2 = joblib.load (PREP2_FILE_NAME)
clf = joblib.load (MODEL_FILE_NAME)
print ("Modelo carregado e pré-processadores")
# executar a previsão
X1 = pre1.transform (data)
X2 = pre2.transform (X1)
saída = dict (zip (dados, clf.predict (X2) [:]))
saída de retorno
 def lambda_handler (evento, contexto): 
predictList = []
para registro no evento ['Records']:
# decodificar os dados do base64 Kinesis
decoded_record_data = base64.b64decode (record ['kinesis'] ['dados'])
# carrega o registro do banco de dados do dínamo
deserialized_data = json.loads (decoded_record_data)
predictList.append (json.loads (deserialized_data ['textblob']))
result = predict (predictList)
resultado de retorno

Mais uma vez, a função será compactada e depois publicada. Assume-se que a função subjacente já foi criada:

 zip MyModel20190104v1_lambda_function.zip lambda_function.py 
 aws lambda create-function --função-nome MyModelInference --runtime python3.6 --handler lambda_function.lambda_handler --role arn: aws: iam :: xxxxxxxxxxx: papel / MyModelInference_role --zip-arquivo fileb: //MyModel20190104v1_lambda_function.zip 

Vincular as camadas

As camadas precisam agora estar ligadas à função gerada anteriormente usando os ARNs:

 aws lambda update-function-configuration - nome-da-função MyModelInference --layer arn: aws: lambda: eu-west-1: xxxxxxxxxxx: camada: MyMLfunction_layer1: 1 arn: aws: lambda: eu-oeste-1: xxxxxxxxxxx: camada : MyMLfunction_layer2: 1 

Aqui é usada a “versão 1” inicial da camada, que é visível com o número após os dois pontos.

Atualizar (apenas) as camadas do modelo

Se uma versão nova e melhorada do modelo for gerada, ela deverá ser carregada novamente:

 zip MyMLfunction_layer2_model20190105v1.zip * .joblib 
 aws lambda publicar-camada-versão --layer-name MyMLfunction_layer2 - arquivo -zip fileb: //MyMLfunction_layer2_model20190105v1.zip --compatible-runtimes python3.6 

e a configuração da camada da função deve ser atualizada:

 aws lambda update-function-configuration - nome-da-função MyModelInference --layer arn: aws: lambda: eu-west-1: xxxxxxxxxxx: camada: MyMLfunction_layer1: 1 arn: aws: lambda: eu-oeste-1: xxxxxxxxxxx: camada : MyMLfunction_layer2: 2 

Após esse passo, o novo modelo é implantado com a nossa função lambda de inferência ML.

Observações de execução e custo

Como mencionado na introdução deste artigo, fizemos algumas investigações sobre duração e custo de execução. Esse tipo de análise é essencial na maioria dos projetos e é um requisito se você estiver seguindo a estrutura bem arquitetada da AWS .

Em nosso caso de uso, a entrada de dados é baseada no processo em lote e ocorre apenas algumas vezes por dia. Em cada entrada, serão gerados 50.000 elementos de dados (eventos Kinesis) e, portanto, múltiplos picos curtos de 50k previsões cada um terá que ser executado durante o dia.

Tempo de execução

A função foi acionada com diferentes tamanhos de lote de 1, 10 e 100 para ver a diferença geral no tempo de execução.

  • 1 previsão : Tempo de execução médio 3803 ms, duração da fatura: 3900 ms , Memória máxima usada: 151 MB
  • 10 previsões : Tempo de execução médio 3838 ms, duração da fatura: 3900 ms , Memória máxima usada: 151 MB
  • 100 previsões : Tempo de execução médio 3912 ms, duração da fatura: 4000 ms , Memória máxima: 151 MB

O tempo de carregamento e inicialização é, naturalmente, o fator limitante, pois o tempo de execução geral não muda muito com mais previsões. Nesse caso, selecionaríamos um tamanho de lote de 100. Para investigações futuras, também veremos como podemos executar a função de maneira mais otimizada, talvez com um tempo de execução personalizado, para diminuir essa duração de inicialização e também para criar tamanhos de lote menores eficaz.

Custo de execução

Como dito no início, a opção de implementação de ML tradicional na AWS é hospedar o modelo via Sagemaker. Se o menor tamanho de instância ml.t2.medium for escolhido, um custo por hora de $ 0,07 será cobrado, o que seria uma taxa mensal de $ 50,40 para um tempo de execução 24/7, então este é nosso limite inferior para comparação, pois para cargas mais altas e instâncias caras têm que ser usadas.

Com o AWS Lambda, um nível gratuito está disponível e, com nosso tamanho de tempo de execução escolhido de 192 MB, isso seria 2.133.333 segundos. Após esse nível gratuito, aplica-se um preço por 100 ms de US $ 0,000000313.

Assumindo agora uma execução de função de 4s com qualquer tamanho de lote entre 1 e 100, chegamos a um custo de $ 0,0000124 por execução.

Tomando a única ingestão de 50000 elementos significaria que tal lote custa:

  • Tamanho do lote 1 : 50000 x US $ 0,0000124 / 1 = US $ 0,62 por ingestão
  • Tamanho do lote 10 : 50000 x $ 0,0000124 / 10 = $ 0,062 por ingest
  • Tamanho do lote 100 : 50000 x $ 0,0000124 / 100 = $ 0,0062 por ingest

Vamos comparar quantos ingerimos com o tamanho de lote 100 que podemos fazer por mês até atingirmos os custos da menor instância de ML hospedada de $ 50.40:

US $ 50,40 / US $ 0,0062 = 8129 ingere

Isso seria 271 ingesta por dia ou 11 ingest por hora antes que nossa função lambda fosse mais cara.

Conclusão

Executar a inferência de ML usando uma abordagem completa sem servidor pode ser muito eficaz. Se o tempo de execução puder ser limitado e o tamanho de lote correto for usado, ele será mais acessível do que um modelo e um terminal permanentemente hospedados. Além disso, a ausência de servidor permite até mesmo o manuseio automático de picos de carga sem a configuração / lógica de dimensionamento personalizado ou a adaptação do tamanho da instância hospedada.

O uso de uma abordagem em camadas nos permite implementar de maneira independente e fácil novos modelos sem tocar e implantar o código completo. Claro, também é fácil reverter para uma versão anterior.

Construir através de contêineres docker nos permite ter um tempo de execução consistente para as funções. Como Kelsey Hightower mostrou durante sua palestra KubeCon 2018: Kubernetes e o caminho para o Serverless ( Recomendado para assistir! ), É possível extrair uma camada de imagem do docker e carregá-la para gerar o tempo de execução personalizado. Eu acho que vamos ver mais neste espaço em breve!