<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Devalent</title>
  
  
  <link href="https://devalent.com/blog/atom.xml" rel="self"/>
  
  <link href="https://devalent.com/blog/"/>
  <updated>2025-04-06T17:19:46.392Z</updated>
  <id>https://devalent.com/blog/</id>
  
  <author>
    <name>Devalent</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Machine to Machine Communication of AI Agents in AWS Bedrock</title>
    <link href="https://devalent.com/blog/machine-to-machine-communication-of-ai-agents-in-aws-bedrock/"/>
    <id>https://devalent.com/blog/machine-to-machine-communication-of-ai-agents-in-aws-bedrock/</id>
    <published>2024-01-20T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.392Z</updated>
    
    <content type="html"><![CDATA[<p>Building complex AI-powered applications often requires that the system has to parse and process an output of an AI. Previously, Amazon Bedrock developers had to instruct the AI to print JSON or XML as the response in their model prompts, which was error-prone and often contained hallucinations. With the introduction of Bedrock Agents, developers can define schemas in the OpenAPI format and instruct their AI to use external APIs to retrieve the required data or to perform the requested operations.</p><span id="more"></span><h1 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h1><p>In this article we’re going to build a serverless API that can answer any question. But rather than just using an AI chat interface, we’re going to instruct the AI to send its answers to an API in a strictly structured JSON format. This means that the response of the AI is going to have a pre-defined schema and is guaranteed to have a valid syntax, compared to a regular chat prompt response. Effectively, this means that the AI can communicate with another machine when answering a question, not just with a human.</p><p>The source code for this project is <a href="https://github.com/Devalent/m2m-ai-agents-on-aws-bedrock">available at GitHub</a>. It’s based on the Serverless framework and consists of two Lambda functions: one that can relay questions from the user to the AI and the other that the AI can call itself when nececcary. Answers are stored in a DynamoDB table and shared between those two Lambdas:</p><p><img src="/blog/machine-to-machine-communication-of-ai-agents-in-aws-bedrock/diagram.svg" alt="AWS diagram"></p><h1 id="Implementation"><a href="#Implementation" class="headerlink" title="Implementation"></a>Implementation</h1><p>When creating a Bedrock model, we’re going to use a very simple instruction for the AI:</p><blockquote><p>You are an agent that answers questions. You must save the answer to POST::API::answerQuestion API.</p></blockquote><p>And when asking questions, the prompt will be the following:</p><blockquote><p>Save the answer the following question: &lt;question&gt;{question}&lt;&#x2F;question&gt;</p></blockquote><p>Notice the <code>POST::API::answerQuestion</code> part. It is the name of one of the APIs available to the AI. Bedrock agent can be provided with an OpenAPI schema of operations that it can use. The schema for our project looks like this:</p><figure class="highlight json"><figcaption><span>Schema of the agent API</span><a href="https://github.com/Devalent/m2m-ai-agents-on-aws-bedrock/blob/main/src/resources/openapi.ts">source</a></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;openapi&quot;</span>: <span class="string">&quot;3.0.0&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;info&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;version&quot;</span>: <span class="string">&quot;1.0.0&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;title&quot;</span>: <span class="string">&quot;AI agent API&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;APIs available for AI agents.&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;paths&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;/answer-question&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;post&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;summary&quot;</span>: <span class="string">&quot;API to save answers to questions&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Save answers to questions.&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;operationId&quot;</span>: <span class="string">&quot;answerQuestion&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;requestBody&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;required&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">          <span class="attr">&quot;content&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;application/json&quot;</span>: &#123;</span><br><span class="line">              <span class="attr">&quot;schema&quot;</span>: &#123;</span><br><span class="line">                <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">                <span class="attr">&quot;required&quot;</span>: [<span class="string">&quot;answer&quot;</span>],</span><br><span class="line">                <span class="attr">&quot;properties&quot;</span>: &#123;</span><br><span class="line">                  <span class="attr">&quot;answer&quot;</span>: &#123;</span><br><span class="line">                    <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">                    <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Answer to the question.&quot;</span></span><br><span class="line">                  &#125;</span><br><span class="line">                &#125;</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">&quot;responses&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;200&quot;</span>: &#123; <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Answer saved successfully.&quot;</span> &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The schema only provides the structure of available operations, agents can’t call an API directly and instead rely on a Lambda function for execution. Our Lambda function for handling agent API calls has the following code:</p><figure class="highlight typescript"><figcaption><span>Agent API Lambda</span><a href="https://github.com/Devalent/m2m-ai-agents-on-aws-bedrock/blob/main/src/functions/api/handler.ts">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> router = <span class="keyword">new</span> Router();</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> api = <span class="keyword">async</span> (event) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> router.handle(event.httpMethod, event.apiPath, event);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">router.add(<span class="string">&#x27;POST&#x27;</span>, <span class="string">&#x27;/answer-question&#x27;</span>, <span class="keyword">async</span> (request: AgentRequest) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> input = getAgentInput&lt;AnswerQuestionInput&gt;(request); <span class="comment">// Parse the request</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> putDocument(answersTable, &#123;</span><br><span class="line">    <span class="attr">id</span>: request.sessionId,</span><br><span class="line">    <span class="attr">answer</span>: input.answer,</span><br><span class="line">  &#125;); <span class="comment">// Save the answer to DB</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> createAgentResponse(request, &#123; <span class="attr">status</span>: <span class="string">&#x27;OK&#x27;</span> &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>It saves the answer from the AI in a DynamoDB table so that in can be retrieved later. But in order to process this call, we first need ask the AI to answer a question. We do that via another Lambda function that handles requests from a REST endpoint in API Gateway:</p><figure class="highlight typescript"><figcaption><span>Question API Lambda</span><a href="https://github.com/Devalent/m2m-ai-agents-on-aws-bedrock/blob/main/src/functions/question/handler.ts">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> api = <span class="keyword">async</span> (event) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> inputText = textQuestionPrompt <span class="comment">// Put question into the prompt template</span></span><br><span class="line">    .replace(<span class="string">&#x27;&#123;question&#125;&#x27;</span>, event.body.question);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Call Bedrock agent with the text</span></span><br><span class="line">  <span class="keyword">const</span> &#123; completion, sessionId &#125; = <span class="keyword">await</span> invokeAgent(inputText);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> <span class="keyword">await</span> (<span class="keyword">const</span> response <span class="keyword">of</span> completion) &#123;</span><br><span class="line">    <span class="keyword">if</span> (response.trace?.trace) &#123;</span><br><span class="line">      <span class="comment">// Output trace info from the agent</span></span><br><span class="line">      <span class="built_in">console</span>.log(<span class="built_in">JSON</span>.stringify(response.trace.trace));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Read the saved answer from DB</span></span><br><span class="line">  <span class="keyword">const</span> <span class="built_in">document</span> = <span class="keyword">await</span> readDocument(answersTable, sessionId);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> formatJSONResponse(<span class="built_in">document</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Let’s try to call this function with a simple math question:</p><figure class="highlight json"><figcaption><span>Question API request</span><a href="https://github.com/Devalent/m2m-ai-agents-on-aws-bedrock/blob/main/src/functions/question/mock.json">source</a></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;question&quot;</span>: <span class="string">&quot;Betty had a pack of 25 pencil crayons. She gave five to her friend Theresa. She gave three to her friend Mary. How many pencil crayons does Betty have left?&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>If tracing is enabled, the AI should log its reasoning and implementation details:</p><blockquote><p>To answer this question, I will:</p><ol><li>Solve the math problem: Betty originally had 25 pencil crayons. She gave 5 to Theresa and 3 to Mary. So she gave away 5 + 3 &#x3D; 8 pencil crayons. So she has 25 - 8 &#x3D; 17 pencil crayons left.</li><li>Call the POST::m2m-ai-agent-api::answerQuestion function to save the answer.</li></ol><p>I have checked that I have access to the POST::m2m-ai-agent-api::answerQuestion function.<br>&lt;function_call&gt;post::m2m-ai-agent-api::answerQuestion(answer&#x3D;&quot;17&quot;)&lt;&#x2F;function_call&gt;<br>&lt;function_result&gt;{&quot;status&quot;:&quot;OK&quot;}&lt;&#x2F;function_result&gt;</p></blockquote><p>And the API response will contain the answer to the original question:</p><figure class="highlight json"><figcaption><span>Question API response</span></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;643dc42c-f492-43ce-bfb8-ac991b2090e5&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;answer&quot;</span>: <span class="string">&quot;17&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Agents for Amazon Bedrock is a powerful feature that can bring AI applications to a whole new level. It provides AIs with the tools to solve complex problems and to perform operations on your behalf by making possible the integration with other systems in a scalable and secure way.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Building complex AI-powered applications often requires that the system has to parse and process an output of an AI. Previously, Amazon Bedrock developers had to instruct the AI to print JSON or XML as the response in their model prompts, which was error-prone and often contained hallucinations. With the introduction of Bedrock Agents, developers can define schemas in the OpenAPI format and instruct their AI to use external APIs to retrieve the required data or to perform the requested operations.&lt;/p&gt;</summary>
    
    
    
    
    <category term="AWS" scheme="https://devalent.com/blog/tags/AWS/"/>
    
    <category term="Cloud Computing" scheme="https://devalent.com/blog/tags/Cloud-Computing/"/>
    
    <category term="Machine Learning" scheme="https://devalent.com/blog/tags/Machine-Learning/"/>
    
    <category term="Serverless" scheme="https://devalent.com/blog/tags/Serverless/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
    <category term="AI" scheme="https://devalent.com/blog/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>Multi-Tenant GPU-Accelerated AWS Applications</title>
    <link href="https://devalent.com/blog/multitenant-gpu-accelerated-aws-applications/"/>
    <id>https://devalent.com/blog/multitenant-gpu-accelerated-aws-applications/</id>
    <published>2022-03-27T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.394Z</updated>
    
    <content type="html"><![CDATA[<p>The first generation of GPU instances was introduced by AWS back in 2010. The industry has changed dramatically since then and nowadays server-side GPUs are widely used for a broad range of tasks - from machine learning to media processing. In this article we’re going to build a server that can securely run graphical applications in isolated environments and leverage on the power of modern GPUs.</p><span id="more"></span><h1 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h1><p>The application that we’re going to build is a webpage video recorder. The architecture is pretty straightforward: we will deploy an AWS ECS cluster of GPU-powered instances. The system image already has NVIDIA drivers installed and available from within Docker with just a little effort, so we will focus on the application itself. The application contains a pool of virtual displays. Whenever a website recording is requested, an isolated Chrome instance is launched. When it loads the webpage, the server captures the screen and encodes it in realtime to a video file on the GPU.</p><p><img src="/blog/multitenant-gpu-accelerated-aws-applications/schema.svg" alt="Schema"></p><p>G5 instances are equipped with a powerful NVIDIA A10G GPU which allows to process several HD video streams simultaneously, so the application can easily scale up on the number of virtual displays, and thanks to Docker, it can run isolated parallel workflows in a secure manner.</p><p>The source code for this project is <a href="https://github.com/Devalent/multitenant-gpu-on-aws">available on GitHub</a>. It contains the application Docker image and the AWS infrastructure definition.</p><h1 id="Docker-Container"><a href="#Docker-Container" class="headerlink" title="Docker Container"></a>Docker Container</h1><p>The container is based on the official NVIDIA CUDA image that contains everything that we will need to leverage on the hardware acceleration. As for the AWS instance image, we will use the official ECS-optimized AMI for GPU instances, which contains fresh NVIDIA drivers and libraries, a Docker runtime with GPU support and everything else that we’ll need to start using it in an ECS cluster. The only <a href="https://github.com/Devalent/multitenant-gpu-on-aws/blob/main/application/install/ffmpeg.sh">tricky part</a> is to build FFmpeg from the source and to link it with NVIDIA libraries available in the system. It’s also worth noting that <code>NVIDIA_DRIVER_CAPABILITIES</code> variable controls what NVIDIA features will be available to the container.</p><figure class="highlight docker"><figcaption><span>Docker image</span><a href="https://github.com/Devalent/multitenant-gpu-on-aws/blob/main/application/Dockerfile">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># CUDA 11.4 matches the runtime at the latest AWS ECS-optimized AMI for GPU instances</span></span><br><span class="line"><span class="comment"># https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html</span></span><br><span class="line"><span class="comment"># Use the development version to be able to reference headers during FFmpeg build</span></span><br><span class="line"><span class="keyword">FROM</span> nvidia/cuda:<span class="number">11.4</span>.<span class="number">0</span>-devel-ubuntu20.<span class="number">04</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ARG</span> DEBIAN_FRONTEND=noninteractive</span><br><span class="line"></span><br><span class="line"><span class="comment"># Mount all driver libraries</span></span><br><span class="line"><span class="comment"># https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities</span></span><br><span class="line"><span class="keyword">ENV</span> NVIDIA_DRIVER_CAPABILITIES=all</span><br><span class="line"><span class="keyword">ENV</span> PORT=<span class="number">3000</span></span><br><span class="line"><span class="keyword">ENV</span> NODE_ENV=develop</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apt-get update -y &amp;&amp; apt-get install -y curl wget</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> mkdir /install</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /install</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># FFmpeg, build from source and include NVIDIA codecs</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./install/ffmpeg.sh .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> bash -e ffmpeg.sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># VirtualGL</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./install/virtualgl.sh .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> bash -e virtualgl.sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Chrome dependencies</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./install/chrome.sh .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> bash -e chrome.sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Utilities</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./install/utils.sh .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> bash -e utils.sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Node.js</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./install/node.sh .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> bash -e node.sh</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> rm -rf /install</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Application</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /usr/src/app</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> package.json .</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> package-lock.json .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> npm i</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ADD</span><span class="bash"> . .</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> npm run build &amp;&amp; rm -rf src</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> chmod -R o+rwx node_modules/puppeteer/.local-chromium</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> npm start</span></span><br></pre></td></tr></table></figure><h1 id="Application"><a href="#Application" class="headerlink" title="Application"></a>Application</h1><p>The application is a Node.js API server. It launches virtual displays with <a href="https://en.wikipedia.org/wiki/Xvfb">Xvfb</a> (which stands for X virtual framebuffer) and manages the resource pooling. Each display contains a window manager, so that applications can draw themselves on it. The displays also utilize OpenGL provided by <a href="https://en.wikipedia.org/wiki/VirtualGL">VirtualGL</a>. While this is not a truly 3D-accelerated environment and Xvfb is also CPU demanding compared to a regular X server, for the sake of simplicity we won’t discuss how to get around that, since it’s more than enough for our use case.</p><figure class="highlight typescript"><figcaption><span>Display initialization</span><a href="https://github.com/Devalent/multitenant-gpu-on-aws/blob/main/application/src/service/browser/display.ts#L51">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Start the display server</span></span><br><span class="line"><span class="comment">// https://en.wikipedia.org/wiki/Xvfb</span></span><br><span class="line"><span class="comment">// https://en.wikipedia.org/wiki/VirtualGL</span></span><br><span class="line"><span class="keyword">const</span> item = <span class="keyword">await</span> <span class="keyword">new</span> <span class="built_in">Promise</span>&lt;Xvfb&gt;(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> xvfb = <span class="keyword">new</span> Xvfb(&#123;</span><br><span class="line">    <span class="attr">reuse</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">xvfb_args</span>: [</span><br><span class="line">      <span class="string">&#x27;-screen&#x27;</span>, <span class="string">&#x27;0&#x27;</span>, <span class="string">`1920x1080x24+32`</span>,</span><br><span class="line">      <span class="string">&#x27;+extension&#x27;</span>, <span class="string">&#x27;GLX&#x27;</span>, <span class="comment">// Enable OpenGL</span></span><br><span class="line">      <span class="string">&#x27;+extension&#x27;</span>, <span class="string">&#x27;RANDR&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;-nolisten&#x27;</span>, <span class="string">&#x27;tcp&#x27;</span>, <span class="string">&#x27;-dpi&#x27;</span>, <span class="string">&#x27;96&#x27;</span>, <span class="string">&#x27;-ac&#x27;</span>, <span class="string">&#x27;-noreset&#x27;</span>,</span><br><span class="line">    ],</span><br><span class="line">  &#125;);</span><br><span class="line">  xvfb.start(<span class="function">(<span class="params">error</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (error) &#123;</span><br><span class="line">      reject(error);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      resolve(xvfb);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">const</span> display = item.display(); <span class="comment">// Get the display number, e.g. &quot;:99&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Start the window manager</span></span><br><span class="line"><span class="comment">// https://en.wikipedia.org/wiki/Fluxbox</span></span><br><span class="line"><span class="keyword">const</span> wm = execa(<span class="string">&#x27;fluxbox&#x27;</span>, [], &#123;</span><br><span class="line">  <span class="attr">env</span>: &#123; <span class="attr">DISPLAY</span>: display &#125;, <span class="comment">// Run on the new display</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Hide the cursor</span></span><br><span class="line"><span class="comment">// http://manpages.ubuntu.com/manpages/trusty/man1/unclutter.1.html</span></span><br><span class="line"><span class="keyword">const</span> cursor = execa(<span class="string">&#x27;unclutter&#x27;</span>, [<span class="string">&#x27;-idle&#x27;</span>, <span class="string">&#x27;0&#x27;</span>], &#123;</span><br><span class="line">  <span class="attr">env</span>: &#123; <span class="attr">DISPLAY</span>: display &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Chrome is launched using <a href="https://github.com/puppeteer/puppeteer">Puppeteer</a> with quite a few options in order to be able to work inside Docker and to enable the hardware acceleration that is usually disabled in server usage scenarios:</p><figure class="highlight bash"><figcaption><span>Chrome launch command</span><a href="https://github.com/Devalent/multitenant-gpu-on-aws/blob/main/application/src/config.ts#L8">source</a></figcaption><table><tr><td class="code"><pre><span class="line">google-chrome-stable \</span><br><span class="line">  --kiosk --start-fullscreen --autoplay-policy=no-user-gesture-required \</span><br><span class="line">  --hide-scrollbars --disable-infobars --no-default-browser-check \</span><br><span class="line">  --no-sandbox --disable-setuid-sandbox \</span><br><span class="line">  --ignore-gpu-blacklist --ignore-gpu-blocklist \</span><br><span class="line">  --enable-features=VaapiVideoDecoder --enable-accelerated-video-decode \</span><br><span class="line">  --enable-gpu-rasterization --enable-oop-rasterization --enable-tcp-fast-open \</span><br><span class="line">  --use-gl=desktop --enable-webgl</span><br></pre></td></tr></table></figure><p>The resulting GPU report from <code>chrome://gpu</code> looks this way and is similar to what you can see in Chrome on GPU-powered non-Windows platforms. It can vary greatly depending on the Chrome version used and the driver version installed, but this outcome is perfectly fine for our use case:</p><blockquote><p>Canvas: <span class="highlight-text success">Hardware accelerated</span><br>Canvas out-of-process rasterization: <span class="highlight-text danger">Disabled</span><br>Direct Rendering Display Compositor: <span class="highlight-text warning">Disabled</span><br>Compositing: <span class="highlight-text success">Hardware accelerated</span><br>Multiple Raster Threads: <span class="highlight-text success">Enabled</span><br>OpenGL: <span class="highlight-text success">Enabled</span><br>Rasterization: <span class="highlight-text success">Hardware accelerated on all pages</span><br>Raw Draw: <span class="highlight-text warning">Disabled</span><br>Skia Renderer: <span class="highlight-text success">Enabled</span><br>Video Decode: <span class="highlight-text success">Hardware accelerated</span><br>Video Encode: <span class="highlight-text warning">Software only. Hardware acceleration disabled</span><br>Vulkan: <span class="highlight-text danger">Disabled</span><br>WebGL: <span class="highlight-text success">Hardware accelerated</span><br>WebGL2: <span class="highlight-text success">Hardware accelerated</span></p></blockquote><h1 id="Screen-Capture"><a href="#Screen-Capture" class="headerlink" title="Screen Capture"></a>Screen Capture</h1><p>The screen capture is performed by <a href="https://en.wikipedia.org/wiki/FFmpeg">FFmpeg</a> with X11 as the input source. NVIDIA GPU that is used in G5 instances provides <a href="https://en.wikipedia.org/wiki/Nvidia_NVENC">NVENC</a> - a hardware accelerated video encoding module. Since we’ve built a custom version of FFmpeg with CUDA libraries, we can now encode <a href="https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding">HEVC (H.265)</a> videos with FFmpeg entirely on the GPU, offloading the CPU for other tasks. This provides us with the ability to capture and compress HD videos in realtime at high FPS. The command for FFmpeg to capture the screen is the following:</p><figure class="highlight bash"><figcaption><span>FFmpeg screen recording</span><a href="https://github.com/Devalent/multitenant-gpu-on-aws/blob/main/application/src/service/ffmpeg/index.ts#L34">source</a></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment"># https://en.wikipedia.org/wiki/FFmpeg</span></span><br><span class="line"><span class="comment"># https://en.wikipedia.org/wiki/X_Window_System#Key_terms</span></span><br><span class="line">ffmpeg \</span><br><span class="line">  -f x11grab \ <span class="comment"># Capture the X Window System</span></span><br><span class="line">  -hwaccel nvdec -hwaccel_output_format cuda \ <span class="comment"># Enable GPU acceleration with NVDEC</span></span><br><span class="line">  -thread_queue_size 2048 -probesize 10M -analyzeduration 10M \ <span class="comment"># Optimizations</span></span><br><span class="line">  -framerate 30 -s 1920x1080 \ <span class="comment"># Input stream parameters</span></span><br><span class="line">  -i :99.0 \ <span class="comment"># Capture &quot;display&quot; 99 and &quot;screen&quot; 0</span></span><br><span class="line">  -c:v hevc_nvenc \ <span class="comment"># Encode as HEVC (H.265) on GPU with NVENC</span></span><br><span class="line">  -preset fast -movflags +faststart -g 999999 \ <span class="comment"># Optimize for screen capture</span></span><br><span class="line">  ~/output.mp4</span><br></pre></td></tr></table></figure><p>FFmpeg runs for the requested amount of seconds, then stops and the resulting MP4 file is served to the user. The following video has been recorded from <a href="https://codepen.io/akm2/full/AxGzJb">this CodePen demo</a>. Thanks to the hardware acceleration, it is smooth and crisp.</p><!-- endcontent --><div class="figure figure--fullWidth center" ><video class="fig-video" preload="none" controls poster="canvas.jpg" alt=""><source src="canvas.mp4" type="video/mp4"><p>Your browser doesn't support HTML5 Video :/</p></video></div><!-- content --><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>As you can see, the toolset for server-side GPU applications has matured enough that building a simple app can take just a few hours. With the power of cloud scalability and the modern software, you can build things that were hard or even unimaginable just a decade ago.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;The first generation of GPU instances was introduced by AWS back in 2010. The industry has changed dramatically since then and nowadays server-side GPUs are widely used for a broad range of tasks - from machine learning to media processing. In this article we’re going to build a server that can securely run graphical applications in isolated environments and leverage on the power of modern GPUs.&lt;/p&gt;</summary>
    
    
    
    
    <category term="AWS" scheme="https://devalent.com/blog/tags/AWS/"/>
    
    <category term="Cloud Computing" scheme="https://devalent.com/blog/tags/Cloud-Computing/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
  </entry>
  
  <entry>
    <title>Automated Predictions with Machine Learning on AWS</title>
    <link href="https://devalent.com/blog/automated-predictions-with-machine-learning-on-aws/"/>
    <id>https://devalent.com/blog/automated-predictions-with-machine-learning-on-aws/</id>
    <published>2022-03-02T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.373Z</updated>
    
    <content type="html"><![CDATA[<p>Building and operating a machine learning system is a process that consists of many components and involves multiple specialists with various skill sets. The iterative nature and constant evolution of such systems amplifies the complexity to the point that it could become challenging to handle it even for a large and experienced team. Luckily, with the right tools at hands, it is possible to streamline this journey. Let’s take a look on how such pipeline could be built from the ground up.</p><span id="more"></span><h1 id="Business-Domain"><a href="#Business-Domain" class="headerlink" title="Business Domain"></a>Business Domain</h1><p>This system operates in the online advertisement domain, which has the following entities:</p><ul><li>Campaign - a sales funnel that includes two or more offers.</li><li>Offer - a product or a service that is advertised to a user by a banner image or a webpage.</li><li>Impression - a single advertisement display to a user. It costs money for the advertiser and one of the goals is to have the highest conversion rate possible.</li><li>Conversion - a successful sale of the offer.</li><li>Conversion rate - the number of conversions divided by the total number of impressions.</li></ul><p>The goal of the system is to predict which offer within a campaign has the highest posibility to convert for a specific user. It does so based on the historical data of conversions for each campaign. The assumption is that user metadata such as their geographical location and smartphone or computer traits correlate with the offer that they could be interested in. This is often the case when an offer is targeted for a specific country, mobile carrier or even a phone vendor.</p><h1 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h1><p>The source code for this project alongside with the installation instructions is available in <a href="https://github.com/Devalent/aws-realtime-predictions">this GitHub repository</a>.</p><p>The architecture involves three data flows: historical performance data is uploaded to a storage, realtime prediction and model training requests are routed to an API. Historical data is used to create machine learning models that perform predictions whenever a training request is received.</p><p><img src="/blog/automated-predictions-with-machine-learning-on-aws/schema.svg" alt="Schema"></p><p>The whole system is serverless, except for the SageMaker inference endpoint. Amazon has recently announced serverless endpoints as well, but they don’t support multi-model deployments that this system relies upon. Each campaign gets its own ML model and all models are deployed into a single inference endpoint. This allows the system to scale to hundreds of campaigns per server, which makes it more economically feasible compared to a single-model deployment when each model requires its own server.</p><h1 id="Data-Format"><a href="#Data-Format" class="headerlink" title="Data Format"></a>Data Format</h1><p>Dataset consists of conversion events in JSON format with the following structure:</p><figure class="highlight json"><figcaption><span>Raw data</span><a href="https://raw.githubusercontent.com/Devalent/aws-realtime-predictions/main/data/conversions.json">source</a></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;b7e7ec22-5143-48b2-bd40-19b0905e8db4&quot;</span>,        <span class="comment">// Conversion ID</span></span><br><span class="line">  <span class="attr">&quot;campaign&quot;</span>: <span class="string">&quot;97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b&quot;</span>,  <span class="comment">// Campaign ID</span></span><br><span class="line">  <span class="attr">&quot;offer&quot;</span>: <span class="string">&quot;41183105-2ed4-46c6-b654-2f0081aa652d&quot;</span>,     <span class="comment">// Offer ID</span></span><br><span class="line">  <span class="attr">&quot;country&quot;</span>: <span class="string">&quot;PL&quot;</span>,                                     <span class="comment">// Country code</span></span><br><span class="line">  <span class="attr">&quot;city&quot;</span>: <span class="string">&quot;Warsaw&quot;</span>,                                    <span class="comment">// City name</span></span><br><span class="line">  <span class="attr">&quot;long&quot;</span>: <span class="string">&quot;21.0026&quot;</span>,                                   <span class="comment">// Longitude</span></span><br><span class="line">  <span class="attr">&quot;lat&quot;</span>: <span class="string">&quot;52.2484&quot;</span>,                                    <span class="comment">// Latitude</span></span><br><span class="line">  <span class="attr">&quot;isp&quot;</span>: <span class="string">&quot;T-Mobile Polska S.A.&quot;</span>,                       <span class="comment">// Internet service provider</span></span><br><span class="line">  <span class="attr">&quot;network&quot;</span>: <span class="string">&quot;188.146.0.0/15&quot;</span>,                         <span class="comment">// ISP network range</span></span><br><span class="line">  <span class="attr">&quot;dType&quot;</span>: <span class="string">&quot;mobile&quot;</span>,                                   <span class="comment">// Device type</span></span><br><span class="line">  <span class="attr">&quot;dOs&quot;</span>: <span class="string">&quot;Android&quot;</span>,                                    <span class="comment">// Operating system</span></span><br><span class="line">  <span class="attr">&quot;dOsVersion&quot;</span>: <span class="string">&quot;7.0&quot;</span>,                                 <span class="comment">// OS version</span></span><br><span class="line">  <span class="attr">&quot;dBrowser&quot;</span>: <span class="string">&quot;Chrome WebView&quot;</span>,                        <span class="comment">// Web browser</span></span><br><span class="line">  <span class="attr">&quot;dBrowserVersion&quot;</span>: <span class="string">&quot;65.0.3325.109&quot;</span>                   <span class="comment">// Web browser version</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This data is calculated based on client’s <a href="https://en.wikipedia.org/wiki/User_agent#Use_in_HTTP">user agent</a> and IP address, processed with an IP lookup database to estimate their geographical location and <a href="https://en.wikipedia.org/wiki/Internet_service_provider">ISP</a>.</p><p>There are two offers in the dataset and only one campaign - <code>97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b</code>.</p><h1 id="Data-Extraction"><a href="#Data-Extraction" class="headerlink" title="Data Extraction"></a>Data Extraction</h1><p>In order to start making predictions for a campaign, we need to prepare a ML model for it. When and under what conditions a model preparation should be triggered is outside of the scope of this article. It could be based on a schedule or when a certain advertisement campaign threshold is reached, but we’re going to request it manually. In order to start the ML pipeline, you need call the API with a campaign ID:</p><figure class="highlight bash"><figcaption><span>Start the ML training pipeline</span></figcaption><table><tr><td class="code"><pre><span class="line">curl https://<span class="variable">$API_GATEWAY_HOST</span>/train/97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b</span><br></pre></td></tr></table></figure><figure class="highlight json"><figcaption><span>Training API response</span></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;step_machine_url&quot;</span>: <span class="string">&quot;https://us-west-2.console.aws.amazon.com/states/home?region=us-west-2#/executions/details/...&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;model_id&quot;</span>: <span class="string">&quot;69ff7307-c1ab-43b5-97b0-a77380a778df&quot;</span> <span class="comment">// This is a ML model ID and also a SageMaker Pipeline execution name</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>It will spin up an AWS Step Function, which is responsible for extracting the data from a data lake and starting the SageMaker pipeline. The data is extracted using AWS Athena - a managed service that allows you to run SQL queries over distributed data sources, including traditional relational databases, NoSQL databases and, just like in this case, an S3 data lake with AWS Glue. Using Athena allows to easily switch to any supported <a href="https://en.wikipedia.org/wiki/Database#Database_management_system">DBMS</a> depending on the business needs and to also use plain JSON files without any servers, which is ideal for this tutorial.</p><p>The Athena query is pretty straightforward:</p><figure class="highlight sql"><figcaption><span>Athena SQL query</span><a href="https://github.com/Devalent/aws-realtime-predictions/blob/main/infrastructure/lib/infrastructure-stack.ts#L306">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> country, city, long, lat, network, dos, dtype, </span><br><span class="line">       dosversion, dbrowser, dbrowserversion, offer</span><br><span class="line"><span class="keyword">FROM</span> conversions</span><br><span class="line"><span class="keyword">WHERE</span> campaign <span class="operator">=</span> <span class="string">&#x27;97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b&#x27;</span></span><br></pre></td></tr></table></figure><p>The query results are stored in an S3 bucket and will be provided to the SageMaker pipeline that will be executed on the next step.</p><h1 id="SageMaker-Pipeline"><a href="#SageMaker-Pipeline" class="headerlink" title="SageMaker Pipeline"></a>SageMaker Pipeline</h1><p>SageMaker Pipelines is a managed CI&#x2F;CD service for machine learning. It is defined by the following <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">DAG</a>, each node of which represents a specific ML task.</p><div class="figure center" style="width:;"><img class="fig-img" src="pipeline.png" alt=""></div><h2 id="Data-Wrangling"><a href="#Data-Wrangling" class="headerlink" title="Data Wrangling"></a>Data Wrangling</h2><p>The first step of the pipeline is data preparation (or <a href="https://en.wikipedia.org/wiki/Data_wrangling">wrangling</a>) using AWS Data Wrangler, which has its own DAG:</p><div class="figure center" style="width:;"><img class="fig-img" src="dag.png" alt=""></div><p>On the first step of the process we’re going to get rid of rarely used web browsers and operating systems, since we will encode all unique values into separate columns and we don’t want to get a very sparse matrix as the result. Another reason for that is due to the nature of the business domain, such outliers often indicate bot traffic or traffic source targeting issues, and should be filtered out.</p><p>The histogram for web browsers before applying filtering looks like that:</p><p><img src="/blog/automated-predictions-with-machine-learning-on-aws/browser-before.png"></p><p>Filtering out web browsers that account for less than 1% of the dataset provides the following result:</p><p><img src="/blog/automated-predictions-with-machine-learning-on-aws/browser-after.png"></p><p>The same applies to countries. Most advertisement campaigns are usually targeted to a specific list of countries and displaying an ad to a user that does not belong to that list will prevent a conversion and will result in wasted money. Low conversion rate can also indicate that the specific campaign is just not very effective in this country, so it makes sense to cut those losses and focus on more promising locations.</p><p>In our case there are quite a few countries that don’t convert much:</p><p><img src="/blog/automated-predictions-with-machine-learning-on-aws/country-before.png"></p><p>Applying the same 1% filter changes the picture drastically:</p><p><img src="/blog/automated-predictions-with-machine-learning-on-aws/country-after.png"></p><p>The next step is to make sure that categorical columns do not contain any characters that can result in processing errors. To achieve that, we will <a href="https://en.wikipedia.org/wiki/Percent-encoding">percent-encode</a> them with a PySpark script:</p><figure class="highlight python"><figcaption><span>URL-encode values for categorical columns</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> urllib</span><br><span class="line"><span class="keyword">from</span> pyspark.sql.functions <span class="keyword">import</span> udf</span><br><span class="line"><span class="keyword">from</span> pyspark.sql.types <span class="keyword">import</span> StringType</span><br><span class="line"></span><br><span class="line">encode_udf = udf(<span class="keyword">lambda</span> x: urllib.parse.quote(x, safe=<span class="string">&#x27;&#x27;</span>), StringType())</span><br><span class="line"></span><br><span class="line">df = df.withColumn(<span class="string">&#x27;dos&#x27;</span>, encode_udf(<span class="string">&#x27;dos&#x27;</span>))</span><br><span class="line">df = df.withColumn(<span class="string">&#x27;dbrowser&#x27;</span>, encode_udf(<span class="string">&#x27;dbrowser&#x27;</span>))</span><br><span class="line"><span class="comment"># Mobile Safari -&gt; Mobile%20Safari</span></span><br></pre></td></tr></table></figure><p>The last step of the common branch of the DAG is to encode categorical columns. In order to train and use an ML model, we need to transform (or encode) non-numerical values to numeric representations. We will use two different approaches for offers and for all other categorical columns that we have. The offers column will be encoded using an indexer, which produces a single column that contains integers, corresponding to a specific offer:</p><figure class="highlight plaintext"><figcaption><span>Encoded offers</span></figcaption><table><tr><td class="code"><pre><span class="line">index,offer</span><br><span class="line">0,012a096e-f414-4da7-bbdc-236da34bb545</span><br><span class="line">1,41183105-2ed4-46c6-b654-2f0081aa652d</span><br></pre></td></tr></table></figure><p>As for the remaining categorical columns (country, OS, device type and web browser), we will use a <a href="https://en.wikipedia.org/wiki/One-hot#Machine_learning_and_statistics">one-hot encoding</a> approach, resulting in a new column (with values of <code>1</code> or <code>0</code>) created for each unique value within a category:</p><figure class="highlight plaintext"><figcaption><span>One-hot encoded categorical columns</span></figcaption><table><tr><td class="code"><pre><span class="line">country_TH,country_NP,country_BD,country_SA,country_DZ,country_VN,country_US,country_MA,country_ZA,country_MX,country_KE,country_JO,country_AE,country_AZ,country_IN,country_PL,country_ES,country_NG,country_LB,country_CL,country_FR,country_PE,country_NL,country_ID,dos_Android,dos_iOS,dtype_mobile,dtype_tablet,dtype_desktop,dbrowser_Chrome,dbrowser_Chrome%20WebView,dbrowser_Android%20Browser,dbrowser_UCBrowser,dbrowser_Samsung%20Browser,dbrowser_Facebook,dbrowser_Mobile%20Safari</span><br></pre></td></tr></table></figure><p>The latitude and longitue columns will remain as is and will be included in the output. The city column will be omitted, because using only latitude and longitue provided similar results in our preliminary research. ISP, network, browser and OS version columns will be removed, since they produce a very sparse model and does not seem to correlate with the offer ID.</p><p>At this point we have reached the end of the common branch and the next steps will reflect transformations intended for a specific DAG output.</p><h3 id="Data-Wrangler-Output"><a href="#Data-Wrangler-Output" class="headerlink" title="Data Wrangler Output"></a>Data Wrangler Output</h3><p>The first output that we need is a list of columns. To produce it, we’re just going to limit the number of rows intended for this output to one, so that the processing step won’t have to load a huge dataset file in order to just to get a list of columns in it:</p><figure class="highlight sql"><figcaption><span>Export columns</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> df <span class="keyword">TABLESAMPLE</span>(<span class="number">1</span> <span class="keyword">ROWS</span>)</span><br></pre></td></tr></table></figure><p>The exact same approach is used to get the second output - a list of encoded offers that we created earlier. The only difference is that the output will only contain two columns (index and offer ID) and everything else will be removed.</p><p>The last three outputs are samples of our dataset for ML model training, validation and testing. They are created by splitting the dataset in a proportion of 70%, 15% and 15% respectively:</p><figure class="highlight python"><figcaption><span>Split into train, validation and test</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">train, validation, test = df.randomSplit(weights = [<span class="number">0.70</span>, <span class="number">0.15</span>, <span class="number">0.15</span>], seed = <span class="number">13</span>)</span><br><span class="line">df = train</span><br></pre></td></tr></table></figure><p>That concludes the process of data preparation and we now approach the next step in our SageMaker pipeline.</p><h2 id="Preprocessing"><a href="#Preprocessing" class="headerlink" title="Preprocessing"></a>Preprocessing</h2><p>Data Wrangler operates on rows and columns, but in order to execute the next steps, we need to prepare some metadata. Preprocessing step is repsonsible for reading files with offers and columns data from the Data Wrangler output and converting it to JSON that we can reference in our SageMaker <a href="https://github.com/Devalent/aws-realtime-predictions/blob/main/pipeline/pipeline.py#L273">pipeline definition script</a>.</p><p>Under the hood, it’s just a generic Python environment with various data science libraries pre-installed, that can be used to perform all kinds of data preparation and processing tasks.</p><h2 id="Training"><a href="#Training" class="headerlink" title="Training"></a>Training</h2><p>Once the data is prepared, we can proceed with training the model. We’re going to use XGBoost and create a model that performs a <a href="https://en.wikipedia.org/wiki/Multiclass_classification">multiclass classification</a>. In order to account for the fact that our pipeline is automatic and does not involve any manual tuning, we will run a <a href="https://en.wikipedia.org/wiki/Hyperparameter_optimization">hyperparameter optimization</a>. It will generate several models with different parameters and the best performing model will be deployed to production.</p><figure class="highlight python"><figcaption><span>Hyperparameter tuning</span><a href="https://github.com/Devalent/aws-realtime-predictions/blob/main/pipeline/pipeline.py#L314">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">xgb_estimator.set_hyperparameters(</span><br><span class="line">    objective=<span class="string">&quot;multi:softprob&quot;</span>,</span><br><span class="line">        <span class="comment"># Multiclass with probabilities</span></span><br><span class="line">    eval_metric=<span class="string">&quot;mlogloss&quot;</span>,</span><br><span class="line">        <span class="comment"># Negative log-likelihood</span></span><br><span class="line">        <span class="comment"># https://en.wikipedia.org/wiki/Likelihood_function#Log-likelihood</span></span><br><span class="line">    num_round=<span class="string">&quot;100&quot;</span>,</span><br><span class="line">        <span class="comment"># The number of rounds for boosting</span></span><br><span class="line">    num_class=JsonGet(</span><br><span class="line">        step_name=step_process.name,</span><br><span class="line">        property_file=classes_map,</span><br><span class="line">        json_path=<span class="string">&quot;length_str&quot;</span>,</span><br><span class="line">    ),</span><br><span class="line">        <span class="comment"># Number of classes</span></span><br><span class="line">)</span><br><span class="line">xgb_tuner = HyperparameterTuner(</span><br><span class="line">    estimator=xgb_estimator,</span><br><span class="line">    objective_metric_name=<span class="string">&quot;validation:mlogloss&quot;</span>,</span><br><span class="line">    objective_type=<span class="string">&quot;Minimize&quot;</span>, <span class="comment"># The less log-likelihood is the better</span></span><br><span class="line">    hyperparameter_ranges=&#123;</span><br><span class="line">        <span class="comment"># See https://xgboost.readthedocs.io/en/stable/parameter.html</span></span><br><span class="line">        <span class="string">&quot;alpha&quot;</span>: ContinuousParameter(<span class="number">0.01</span>, <span class="number">10</span>, scaling_type=<span class="string">&quot;Logarithmic&quot;</span>),</span><br><span class="line">            <span class="comment"># It can be used in case of very high dimensionality so that the algorithm runs faster when implemented.</span></span><br><span class="line">            <span class="comment"># Increasing this value will make model more conservative.</span></span><br><span class="line">        <span class="string">&quot;eta&quot;</span>: ContinuousParameter(<span class="number">0.01</span>, <span class="number">0.2</span>),</span><br><span class="line">            <span class="comment"># It is the step size shrinkage used in update to prevent overfitting.</span></span><br><span class="line">            <span class="comment"># After each boosting step, we can directly get the weights of new features,</span></span><br><span class="line">            <span class="comment"># and eta shrinks the feature weights to make the boosting process more conservative.</span></span><br><span class="line">        <span class="string">&quot;gamma&quot;</span>: ContinuousParameter(<span class="number">0.0</span>, <span class="number">0.5</span>),</span><br><span class="line">            <span class="comment"># A node is split only when the resulting split gives a positive reduction in the loss function.</span></span><br><span class="line">            <span class="comment"># Gamma specifies the minimum loss reduction required to make a split.</span></span><br><span class="line">            <span class="comment"># It makes the algorithm conservative. The values can vary depending on the loss function and should be tuned.</span></span><br><span class="line">        <span class="string">&quot;min_child_weight&quot;</span>: ContinuousParameter(<span class="number">1</span>, <span class="number">10</span>),</span><br><span class="line">            <span class="comment"># It defines the minimum sum of weights of all observations required in a child. It is used to control over-fitting.</span></span><br><span class="line">            <span class="comment"># Higher values prevent a model from learning relations which might be highly specific to the particular sample selected for a tree.</span></span><br><span class="line">            <span class="comment"># Too high values can lead to under-fitting. The larger min_child_weight is, the more conservative the algorithm will be.</span></span><br><span class="line">        <span class="string">&quot;max_depth&quot;</span>: IntegerParameter(<span class="number">3</span>, <span class="number">10</span>),</span><br><span class="line">            <span class="comment"># The maximum depth of a tree. It is used to control over-fitting as higher depth will allow model </span></span><br><span class="line">            <span class="comment"># to learn relations very specific to a particular sample.</span></span><br><span class="line">            <span class="comment"># Increasing this value will make the model more complex and more likely to overfit.</span></span><br><span class="line">        <span class="string">&quot;subsample&quot;</span>: ContinuousParameter(<span class="number">0.5</span>, <span class="number">1</span>),</span><br><span class="line">            <span class="comment"># It denotes the fraction of observations to be randomly samples for each tree.</span></span><br><span class="line">            <span class="comment"># Setting it to 0.5 means that XGBoost would randomly sample half of the training data prior to growing trees.</span></span><br><span class="line">            <span class="comment"># This will prevent overfitting. Lower values make the algorithm more conservative and prevents overfitting </span></span><br><span class="line">            <span class="comment"># but too small values might lead to under-fitting.</span></span><br><span class="line">    &#125;,</span><br><span class="line">    max_jobs=<span class="number">10</span>, <span class="comment"># Create up to 10 models</span></span><br><span class="line">    max_parallel_jobs=<span class="number">5</span>, <span class="comment"># 5 parallel training job at a time</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="Evaluation"><a href="#Evaluation" class="headerlink" title="Evaluation"></a>Evaluation</h2><p>Even with the best performing model at hands, we still want to see if it’s worth deploying it to production. The suitable metric to evaluate a multiclass classification model is a <a href="https://en.wikipedia.org/wiki/Hamming_distance">hamming loss</a> - a percentage of samples that were not predicted correctly:</p><figure class="highlight python"><figcaption><span>Hamming loss calculation</span><a href="https://github.com/Devalent/aws-realtime-predictions/blob/main/pipeline/evaluate.py#L67">source</a></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="keyword">import</span> pandas <span class="keyword">as</span> pd</span><br><span class="line"><span class="keyword">from</span> sklearn.metrics <span class="keyword">import</span> hamming_loss</span><br><span class="line"><span class="keyword">import</span> xgboost</span><br><span class="line"></span><br><span class="line"><span class="comment"># ...</span></span><br><span class="line"></span><br><span class="line">df = pd.read_csv(file_test)</span><br><span class="line">y_test = df.iloc[:, <span class="number">0</span>].to_numpy()</span><br><span class="line">df.drop(df.columns[<span class="number">0</span>], axis=<span class="number">1</span>, inplace=<span class="literal">True</span>)</span><br><span class="line">X_test = xgboost.DMatrix(df.values)</span><br><span class="line"></span><br><span class="line">num_class = <span class="built_in">len</span>(np.unique(y_test))</span><br><span class="line">num_samples = y_test.shape[<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line">model = xgboost.Booster()</span><br><span class="line">model.load_model(<span class="string">&#x27;xgboost-model&#x27;</span>)</span><br><span class="line"></span><br><span class="line">predictions = model.predict(X_test)</span><br><span class="line"></span><br><span class="line">predictions = predictions.reshape(num_samples, num_class)</span><br><span class="line">pred_label = np.argmax(predictions, axis=<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">hloss = hamming_loss(y_test, pred_label)</span><br></pre></td></tr></table></figure><p>Our pipeline will only proceed with models that have a Hamming loss of 40% or less. Models that don’t fit into that threshold will be discarded and previously deployed model will remaing in production.</p><h2 id="Deployment"><a href="#Deployment" class="headerlink" title="Deployment"></a>Deployment</h2><p>The last step in the pipeline is model deployment. It is implemented by a Lambda function that loads the model into an S3 bucket that SageMaker inference endpoint uses to access the models, updates the model metadata in a DynamoDB table and performs a cleanup.</p><h1 id="Testing"><a href="#Testing" class="headerlink" title="Testing"></a>Testing</h1><p>We can now finally call our API and get predictions. The API requires only one parameter - a campaign ID:</p><figure class="highlight bash"><figcaption><span>Invoke prediction API</span></figcaption><table><tr><td class="code"><pre><span class="line">curl https://<span class="variable">$API_GATEWAY_HOST</span>/predict/97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b</span><br></pre></td></tr></table></figure><figure class="highlight json"><figcaption><span>Prediction API response</span></figcaption><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// Model input</span></span><br><span class="line">  <span class="attr">&quot;input&quot;</span>: <span class="string">&quot;100.5997,13.5989,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0&quot;</span>,</span><br><span class="line">  <span class="comment">// Model output</span></span><br><span class="line">  <span class="attr">&quot;output&quot;</span>: <span class="string">&quot;[[0.49411922693252563, 0.5058807730674744]]&quot;</span>,</span><br><span class="line">  <span class="comment">// Predicted offer</span></span><br><span class="line">  <span class="attr">&quot;predicted&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;index&quot;</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">&quot;offer&quot;</span>: <span class="string">&quot;41183105-2ed4-46c6-b654-2f0081aa652d&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;probability&quot;</span>: <span class="number">0.5058807730674744</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// Analyzed data</span></span><br><span class="line">  <span class="attr">&quot;source&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;ip&quot;</span>: <span class="string">&quot;16.170.153.144&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;dos&quot;</span>: <span class="string">&quot;Mac OS&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;dosversion&quot;</span>: <span class="string">&quot;10.15.7&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;dbrowser&quot;</span>: <span class="string">&quot;Chrome&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;dbrowserversion&quot;</span>: <span class="string">&quot;98.0.4758.109&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;isp&quot;</span>: <span class="string">&quot;AMAZON-02&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;network&quot;</span>: <span class="string">&quot;16.168.0.0/14&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;country&quot;</span>: <span class="string">&quot;SE&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;city&quot;</span>: <span class="string">&quot;Stockholm&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;long&quot;</span>: <span class="number">18.0717</span>,</span><br><span class="line">    <span class="attr">&quot;lat&quot;</span>: <span class="number">59.3287</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// All predicted offers</span></span><br><span class="line">  <span class="attr">&quot;predictions&quot;</span>: [</span><br><span class="line">    &#123; <span class="attr">&quot;index&quot;</span>: <span class="number">1</span>, <span class="attr">&quot;probability&quot;</span>: <span class="number">0.5058807730674744</span>, <span class="attr">&quot;offer&quot;</span>: <span class="string">&quot;41183105-2ed4-46c6-b654-2f0081aa652d&quot;</span> &#125;,</span><br><span class="line">    &#123; <span class="attr">&quot;index&quot;</span>: <span class="number">0</span>, <span class="attr">&quot;probability&quot;</span>: <span class="number">0.49411922693252563</span>, <span class="attr">&quot;offer&quot;</span>: <span class="string">&quot;012a096e-f414-4da7-bbdc-236da34bb545&quot;</span> &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// Data to model mappings</span></span><br><span class="line">  <span class="attr">&quot;mappings&quot;</span>: &#123;<span class="attr">&quot;long&quot;</span>:<span class="number">100</span>,<span class="attr">&quot;lat&quot;</span>:<span class="number">13</span>,<span class="attr">&quot;country_TH&quot;</span>:<span class="number">1</span>,<span class="attr">&quot;country_NP&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_BD&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_SA&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_DZ&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_VN&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_US&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_MA&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_ZA&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_MX&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_KE&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_JO&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_AE&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_AZ&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_IN&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_PL&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_ES&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_NG&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_LB&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_CL&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_FR&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_PE&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_NL&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;country_ID&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dos_Android&quot;</span>:<span class="number">1</span>,<span class="attr">&quot;dos_iOS&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dtype_mobile&quot;</span>:<span class="number">1</span>,<span class="attr">&quot;dtype_tablet&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dtype_desktop&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_Chrome&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_Chrome%20WebView&quot;</span>:<span class="number">1</span>,<span class="attr">&quot;dbrowser_Android%20Browser&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_UCBrowser&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_Samsung%20Browser&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_Facebook&quot;</span>:<span class="number">0</span>,<span class="attr">&quot;dbrowser_Mobile%20Safari&quot;</span>:<span class="number">0</span>&#125;,</span><br><span class="line">  <span class="comment">// Model metadata</span></span><br><span class="line">  <span class="attr">&quot;endpoint&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;campaign&quot;</span>: <span class="string">&quot;97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;3cb70203-a1e4-7f82-b7b4-b8c89bc67305&quot;</span>,</span><br><span class="line">    <span class="comment">// CSV columns</span></span><br><span class="line">    <span class="attr">&quot;columns&quot;</span>: [<span class="string">&quot;long&quot;</span>,<span class="string">&quot;lat&quot;</span>,<span class="string">&quot;country_TH&quot;</span>,<span class="string">&quot;country_NP&quot;</span>,<span class="string">&quot;country_BD&quot;</span>,<span class="string">&quot;country_SA&quot;</span>,<span class="string">&quot;country_DZ&quot;</span>,<span class="string">&quot;country_VN&quot;</span>,<span class="string">&quot;country_US&quot;</span>,<span class="string">&quot;country_MA&quot;</span>,<span class="string">&quot;country_ZA&quot;</span>,<span class="string">&quot;country_MX&quot;</span>,<span class="string">&quot;country_KE&quot;</span>,<span class="string">&quot;country_JO&quot;</span>,<span class="string">&quot;country_AE&quot;</span>,<span class="string">&quot;country_AZ&quot;</span>,<span class="string">&quot;country_IN&quot;</span>,<span class="string">&quot;country_PL&quot;</span>,<span class="string">&quot;country_ES&quot;</span>,<span class="string">&quot;country_NG&quot;</span>,<span class="string">&quot;country_LB&quot;</span>,<span class="string">&quot;country_CL&quot;</span>,<span class="string">&quot;country_FR&quot;</span>,<span class="string">&quot;country_PE&quot;</span>,<span class="string">&quot;country_NL&quot;</span>,<span class="string">&quot;country_ID&quot;</span>,<span class="string">&quot;dos_Android&quot;</span>,<span class="string">&quot;dos_iOS&quot;</span>,<span class="string">&quot;dtype_mobile&quot;</span>,<span class="string">&quot;dtype_tablet&quot;</span>,<span class="string">&quot;dtype_desktop&quot;</span>,<span class="string">&quot;dbrowser_Chrome&quot;</span>,<span class="string">&quot;dbrowser_Chrome%20WebView&quot;</span>,<span class="string">&quot;dbrowser_Android%20Browser&quot;</span>,<span class="string">&quot;dbrowser_UCBrowser&quot;</span>,<span class="string">&quot;dbrowser_Samsung%20Browser&quot;</span>,<span class="string">&quot;dbrowser_Facebook&quot;</span>,<span class="string">&quot;dbrowser_Mobile%20Safari&quot;</span>],</span><br><span class="line">    <span class="comment">// Category encodings</span></span><br><span class="line">    <span class="attr">&quot;classes&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;012a096e-f414-4da7-bbdc-236da34bb545&quot;</span>: <span class="string">&quot;0&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;41183105-2ed4-46c6-b654-2f0081aa652d&quot;</span>: <span class="string">&quot;1&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// Model evaluation results</span></span><br><span class="line">    <span class="attr">&quot;evaluation&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;hamming_loss&quot;</span>: <span class="number">0.3316062176165803</span> <span class="comment">// Loss rate is 33%</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;modified&quot;</span>: <span class="number">1646122738</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The model predicted that the current user will most likely convert with offer <code>41183105-2ed4-46c6-b654-2f0081aa652d</code> and the possibility that this offer is the best for this user compared to other offers is slightly above 50%. The response also contains detailed information about input and output data, as well as the model information.</p><p>The API also supports an optional <code>input</code> argument that can be used to provide the model with an input directly, without analyzing the current user’s IP address and user agent string. The input format is a CSV without column names and the order of columns must be the same as described in <code>endpoint.columns</code> field:</p><figure class="highlight bash"><figcaption><span>Invoke prediction API with parameters</span></figcaption><table><tr><td class="code"><pre><span class="line">curl https://<span class="variable">$API_GATEWAY_HOST</span>/predict/97318a5c-bbab-4a5a-8a28-c6ab6fcbd79b?input=100.5997,13.5989,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0</span><br></pre></td></tr></table></figure><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Building a machine learning based system is a complex subject that requires a committed effort of a team of engineers and analysts. The ability to sustain constant evolution is one of the most important traits such system must have in order to succeed. Leveraging modern tools and cloud services designed with this in mind opens up a possibility for even small teams to build powerful and sophisticated ML applications.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Building and operating a machine learning system is a process that consists of many components and involves multiple specialists with various skill sets. The iterative nature and constant evolution of such systems amplifies the complexity to the point that it could become challenging to handle it even for a large and experienced team. Luckily, with the right tools at hands, it is possible to streamline this journey. Let’s take a look on how such pipeline could be built from the ground up.&lt;/p&gt;</summary>
    
    
    
    
    <category term="AWS" scheme="https://devalent.com/blog/tags/AWS/"/>
    
    <category term="Cloud Computing" scheme="https://devalent.com/blog/tags/Cloud-Computing/"/>
    
    <category term="Data Analytics" scheme="https://devalent.com/blog/tags/Data-Analytics/"/>
    
    <category term="Machine Learning" scheme="https://devalent.com/blog/tags/Machine-Learning/"/>
    
    <category term="Serverless" scheme="https://devalent.com/blog/tags/Serverless/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
  </entry>
  
  <entry>
    <title>How to Build a Real-time Facial Recognition Service</title>
    <link href="https://devalent.com/blog/how-to-build-a-real-time-facial-recognition-service/"/>
    <id>https://devalent.com/blog/how-to-build-a-real-time-facial-recognition-service/</id>
    <published>2022-02-03T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.382Z</updated>
    
    <content type="html"><![CDATA[<p>Computer vision in general and facial recognition in particular are rapidly growing fields of computer science that can be beneficial in or even revolutionize some areas, such as transportation or retail. In this article we’re going to look closely at how such system could be built.</p><span id="more"></span><p>The <a href="https://github.com/Devalent/facial-recognition-service">source code</a> for this project is available on GitHub. You can run it locally or you can take a look at the <a href="https://devalent.github.io/facial-recognition-app/">demo app</a> to get an idea on how it works. This service does only one thing: it processes a video from a webcam, detects faces and calculates if people it sees have been seen before. This is a generic computer vision task that many applications nowadays leverage upon, such as facial identification on your phone or security systems with access control.</p><p>First, let’s get the bigger picture on how this system operates.</p><h1 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h1><p>The service consists of three distinct parts: a WebRTC server, an application server and a recognition server. Client applications both stream video data and get facial recognition results back over WebRTC. The process can be described with the following sequence diagram:</p><p><img src="/blog/how-to-build-a-real-time-facial-recognition-service/sequence.svg" alt="Sequence diagram"></p><p>Now let’s take a closer look on each actor of the system.</p><h3 id="Client-application"><a href="#Client-application" class="headerlink" title="Client application"></a>Client application</h3><p>The client in this example is a web application built with <a href="https://reactjs.org/">React</a>, <a href="https://redux.js.org/">Redux</a> and <a href="https://nextjs.org/">Next.js</a>, but it is also possible to create a mobile or desktop application, since there are WebRTC implementations for most modern platforms. Even more, with just some small changes, you can process video from IP cameras over RTSP.</p><p>The client application also acts as a temporary store of face data received from the recognition server. In a real-world system, this task will be performed on a server-side and will involve a database, but for the sake of simplicity, this is out of the scope of this article.</p><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling">WebRTC signaling</a> and streaming is performed by the <a href="https://www.npmjs.com/package/openvidu-browser">OpenVidu client</a> over WebSockets, so the WebRTC implementation is trivial.</p><h3 id="WebRTC-server"><a href="#WebRTC-server" class="headerlink" title="WebRTC server"></a>WebRTC server</h3><p>The WebRTC server consists of several building blocks. At its core, it is based on <a href="https://www.kurento.org/">Kurento</a> (a media server) and <a href="https://openvidu.io/">OpenVidu</a> (a signaling server), both are open-source solutions with a paid support for OpenVidu that allows to scale to multiple nodes. The third part of the solution is <a href="https://github.com/coturn/coturn">Coturn</a> – a <a href="https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT">TURN server</a>, which is used for NAT traversal. There are also <a href="https://redis.io/">Redis</a> and <a href="https://www.nginx.com/">NGINX</a> involved internally to handle the state management and to secure communcation between the system components.</p><p>Overall, this solution works out of the box and have unopinionated client libraries available, so we’re not going to dive into the details here. It’s worth mentioning that OpenVidu has recently released a <a href="https://mediasoup.org/">mediasoup</a> support to replace Kurento, which can dramatically improve the overall performance and provide a low-level access for Node.js applications.</p><h3 id="Application-server"><a href="#Application-server" class="headerlink" title="Application server"></a>Application server</h3><p>This server is responsible for the following tasks:</p><ul><li>Provide an API to securely initiate WebRTC sessions. It is possible to initiate a session from the client directly, but that entails security risks and should not be used in production.</li><li>Connect to client video broadcasts and take regular snapshots. The server acts as a WebRTC client and saves video frames to <a href="https://www.npmjs.com/package/canvas">canvas</a> for further processing.</li><li>Provide snapshots to the recognition worker and relay the results back to clients. When the worker responses with face data, the server crops a previously stored snapshot to only include regions that contain faces and sends it to the client alongside with face encodings.</li><li>Host the client web application. This is not a must, but rather a neat feature that Next.js provides.</li></ul><h3 id="Recognition-worker"><a href="#Recognition-worker" class="headerlink" title="Recognition worker"></a>Recognition worker</h3><p>Last but not least, the server that is responsible for implementing facial recognition tasks. It is a Python web server that utilizes the infamous <a href="https://github.com/ageitgey/face_recognition">face_recognition</a> library, which is used to find and to encode faces on images. In this example it’s going to be as simple as that: a stateless server that uses a readily available library to do all the heavy lifting. We’re going to look at possible directions on how to improve upon this solution at the end of the article.</p><h1 id="Algorithm"><a href="#Algorithm" class="headerlink" title="Algorithm"></a>Algorithm</h1><p>Ultimately, we want to be able to tell if two photos contain a face of the same person. But what we’re going to compare, exactly? In order to perform a comparison, we need to transform a photo, which is merely an array of pixels, to something more meaningful. This process is called face encoding. It can vary based on the approach and algorithm used, but overall you can think of it as a measurement of facial features, such as a distance between the eyes, eyebrows, nose or lips, their size and position. The composition of these features can be represented by an N-dimensional vector (in this case, a 128-dimensional one), sometimes called face vector. By calculating the <a href="https://en.wikipedia.org/wiki/Euclidean_distance">Euclidean distance</a> between two face vectors, we can measure how similar (close) those faces (vectors) are.</p><figure class="codeblock codeblock--tabbed"><figcaption><a href="https://github.com/Devalent/facial-recognition-service/blob/main/app/store/recognition/index.ts#L48">recognition.ts</a><ul class="tabs"><li class="tab active">typescript</li><li class="tab">json</li></ul></figcaption><div class="tabs-content"><figure class="highlight typescript" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> newFace = {</span><br><span class="line">  <span class="attr">encodings</span>: [<span class="comment">/* face vector */</span>],</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> candidates = [];</span><br><span class="line"></span><br><span class="line"><span class="comment">// Compare the face with all existing faces</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> existing <span class="keyword">of</span> existingFaces) { </span><br><span class="line">  <span class="comment">// Calculate the Euclidean distance, which is a measure</span></span><br><span class="line">  <span class="comment">// of how two faces are similar to each other.</span></span><br><span class="line">  <span class="comment">// The lower the distance, the more similarity they share.</span></span><br><span class="line">  <span class="keyword">const</span> sum = existing.encodings.reduce(<span class="function">(<span class="params">res, x1, i</span>) =&gt;</span> {</span><br><span class="line">    <span class="keyword">const</span> x2 = newFace.encodings[i];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> res + ((x1 - x2) ** <span class="number">2</span>);</span><br><span class="line">  }, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> distance = <span class="built_in">Math</span>.sqrt(sum);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Only consider faces that are similar enough (distance &lt; 0.6)</span></span><br><span class="line">  <span class="keyword">if</span> (distance &gt;= <span class="number">0</span> &amp;&amp; distance &lt; state.threshold) {</span><br><span class="line">    candidates.push({</span><br><span class="line">      <span class="attr">match</span>: existing,</span><br><span class="line">      distance,</span><br><span class="line">    });</span><br><span class="line">  }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Find a face with the shortest euclidean distance </span></span><br><span class="line"><span class="keyword">const</span> candidate = candidates</span><br><span class="line">  .sort(<span class="function">(<span class="params">a, b</span>) =&gt;</span> a.distance - b.distance)[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// Calculate the similarity percentage</span></span><br><span class="line"><span class="keyword">if</span> (candidate) {</span><br><span class="line">  <span class="keyword">const</span> linear = <span class="number">1.0</span> - (candidate.distance / (state.threshold * <span class="number">2.0</span>));</span><br><span class="line">  <span class="keyword">const</span> score = linear + ((<span class="number">1.0</span> - linear) * <span class="built_in">Math</span>.pow((linear - <span class="number">0.5</span>) * <span class="number">2</span>, <span class="number">0.2</span>));</span><br><span class="line">  <span class="keyword">const</span> similarity = <span class="built_in">Math</span>.round(score * <span class="number">100</span>);</span><br><span class="line"></span><br><span class="line">  alert(<span class="string">`Found a face with the similarity of <span class="subst">${similarity}</span>%`</span>);</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line">  existingFaces.push(newFace);</span><br><span class="line"></span><br><span class="line">  alert(<span class="string">`A new face detected`</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><figure class="highlight json" style="display: none;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// This is an example of a face vector</span></span><br><span class="line">[</span><br><span class="line"> <span class="number">-0.08237294852733612</span>, <span class="number">0.06998157501220703</span>, <span class="number">0.04017249494791031</span>, <span class="number">-0.04595483839511871</span>,</span><br><span class="line"> <span class="number">-0.20524457097053528</span>, <span class="number">0.06130267679691315</span>, <span class="number">-0.003034330904483795</span>, <span class="number">-0.034762077033519745</span>,</span><br><span class="line"> <span class="number">0.07720481604337692</span>, <span class="number">0.0018782643601298332</span>, <span class="number">0.26475948095321655</span>, <span class="number">-0.06688505411148071</span>,</span><br><span class="line"> <span class="number">-0.2565714120864868</span>, <span class="number">-0.024762030690908432</span>, <span class="number">-0.017667800188064575</span>, <span class="number">0.08394743502140045</span>,</span><br><span class="line"> <span class="number">-0.1338515281677246</span>, <span class="number">-0.0945650115609169</span>, <span class="number">-0.10690569132566452</span>, <span class="number">-0.10622785240411758</span>,</span><br><span class="line"> <span class="number">0.02856566198170185</span>, <span class="number">-0.002836777362972498</span>, <span class="number">-0.03754136338829994</span>, <span class="number">0.033261124044656754</span>,</span><br><span class="line"> <span class="number">-0.2024488002061844</span>, <span class="number">-0.25246909260749817</span>, <span class="number">-0.030827492475509644</span>, <span class="number">-0.007754474878311157</span>,</span><br><span class="line"> <span class="number">0.08512576669454575</span>, <span class="number">-0.15835121273994446</span>, <span class="number">0.09394755959510803</span>, <span class="number">0.19072295725345612</span>,</span><br><span class="line"> <span class="number">-0.17289769649505615</span>, <span class="number">-0.04181254655122757</span>, <span class="number">0.04165133833885193</span>, <span class="number">0.14661748707294464</span>,</span><br><span class="line"> <span class="number">-0.11559691280126572</span>, <span class="number">-0.015472346916794777</span>, <span class="number">0.19385945796966553</span>, <span class="number">-0.04230993986129761</span>,</span><br><span class="line"> <span class="number">-0.13715803623199463</span>, <span class="number">0.09014783799648285</span>, <span class="number">0.09632508456707001</span>, <span class="number">0.29142847657203674</span>,</span><br><span class="line"> <span class="number">0.16114553809165955</span>, <span class="number">0.0382876954972744</span>, <span class="number">0.025807470083236694</span>, <span class="number">0.0014453493058681488</span>,</span><br><span class="line"> <span class="number">0.08283577114343643</span>, <span class="number">-0.32194820046424866</span>, <span class="number">0.012574739754199982</span>, <span class="number">0.18525069952011108</span>,</span><br><span class="line"> <span class="number">0.16502265632152557</span>, <span class="number">0.0586332231760025</span>, <span class="number">0.02298889309167862</span>, <span class="number">-0.13721558451652527</span>,</span><br><span class="line"> <span class="number">-0.0279446542263031</span>, <span class="number">0.14717113971710205</span>, <span class="number">-0.14877578616142273</span>, <span class="number">0.15434607863426208</span>,</span><br><span class="line"> <span class="number">0.10199335217475891</span>, <span class="number">-0.21128228306770325</span>, <span class="number">-0.08583835512399673</span>, <span class="number">-0.14078442752361298</span>,</span><br><span class="line"> <span class="number">0.15042155981063843</span>, <span class="number">0.046597909182310104</span>, <span class="number">-0.14368422329425812</span>, <span class="number">-0.1568884551525116</span>,</span><br><span class="line"> <span class="number">0.1736888289451599</span>, <span class="number">-0.088925302028656</span>, <span class="number">-0.11250282824039459</span>, <span class="number">0.16585050523281097</span>,</span><br><span class="line"> <span class="number">-0.06869833171367645</span>, <span class="number">-0.16567328572273254</span>, <span class="number">-0.25068527460098267</span>, <span class="number">0.12233469635248184</span>,</span><br><span class="line"> <span class="number">0.4443138837814331</span>, <span class="number">0.12621524930000305</span>, <span class="number">-0.21290765702724457</span>, <span class="number">-0.00904833059757948</span>,</span><br><span class="line"> <span class="number">-0.028415795415639877</span>, <span class="number">0.003739515785127878</span>, <span class="number">-0.04296185076236725</span>, <span class="number">0.032679952681064606</span>,</span><br><span class="line"> <span class="number">-0.06143368408083916</span>, <span class="number">-0.1341024786233902</span>, <span class="number">-0.1555326282978058</span>, <span class="number">0.04243481904268265</span>,</span><br><span class="line"> <span class="number">0.20805151760578156</span>, <span class="number">-0.0508725643157959</span>, <span class="number">-0.01405704952776432</span>, <span class="number">0.18905237317085266</span>,</span><br><span class="line"> <span class="number">0.1028638407588005</span>, <span class="number">0.04386136680841446</span>, <span class="number">0.0037655732594430447</span>, <span class="number">0.059780303388834</span>,</span><br><span class="line"> <span class="number">-0.11599305272102356</span>, <span class="number">-0.0248013436794281</span>, <span class="number">-0.13408342003822327</span>, <span class="number">-0.01498719397932291</span>,</span><br><span class="line"> <span class="number">0.040145330131053925</span>, <span class="number">-0.13561423122882843</span>, <span class="number">6.418908014893532e-05</span>, <span class="number">0.07700222730636597</span>,</span><br><span class="line"> <span class="number">-0.08549322187900543</span>, <span class="number">0.2858748733997345</span>, <span class="number">-0.01664814166724682</span>, <span class="number">-0.013422049582004547</span>,</span><br><span class="line"> <span class="number">-0.010783687233924866</span>, <span class="number">0.019620651379227638</span>, <span class="number">-0.09562011063098907</span>, <span class="number">0.03741421923041344</span>,</span><br><span class="line"> <span class="number">0.19683292508125305</span>, <span class="number">-0.33195415139198303</span>, <span class="number">0.1915217787027359</span>, <span class="number">0.17226657271385193</span>,</span><br><span class="line"> <span class="number">0.07776990532875061</span>, <span class="number">0.11128266900777817</span>, <span class="number">0.14693227410316467</span>, <span class="number">0.09936179965734482</span>,</span><br><span class="line"> <span class="number">0.037961095571517944</span>, <span class="number">0.017703596502542496</span>, <span class="number">-0.12619523704051971</span>, <span class="number">-0.18041686713695526</span>,</span><br><span class="line"> <span class="number">0.0061234827153384686</span>, <span class="number">-0.09858812391757965</span>, <span class="number">-0.0010826261714100838</span>, <span class="number">0.04442061856389046</span></span><br><span class="line">]</span><br></pre></td></tr></tbody></table></figure></div></figure><h1 id="Going-further"><a href="#Going-further" class="headerlink" title="Going further"></a>Going further</h1><p>While this example is an <a href="https://en.wikipedia.org/wiki/Minimum_viable_product">MVP</a> and doesn’t cut many corners, it still requires some attention before it can be used in production. There are a few things to consider if you’re going to build a real-world service on top of it.</p><h3 id="Machine-learning-pipeline"><a href="#Machine-learning-pipeline" class="headerlink" title="Machine learning pipeline"></a>Machine learning pipeline</h3><p>If you ran this services yourself, you could have noticed that image processing takes a decent amount of time. One of the reasons behind that is <code>face_recognition</code> algorithm used for face detection. The process of face encoding is preceded by face detection. In order to encode facial features, you need to find them on the image first. Instead of relying on the library’s ability to both detect faces and to encode facial features, you could use a faster ML model to detect faces first, then find and encode facial features in a just small region of the image. This becomes critical when processing higher definition images and handling a lot of requests at the same time.</p><p>Moreover, your service could also use specialized ML models to classify faces by age or gender. That opens huge possibilities, but also requires a lot of fine-tuning and performance optimizations.</p><h3 id="Client-side-processing"><a href="#Client-side-processing" class="headerlink" title="Client-side processing"></a>Client-side processing</h3><p>Some of the pipeline steps can (and should, if possible) be offloaded to the client side. Ideally, the client should be responsible for encoding face data, and the only payload to be sent over the network would be a face vector. This greatly reduces the network footprint and gets rid of complex WebRTC infrastructure. Unfortunately, image processing and ML models are very resource-intensive, and not any client hardware can handle it.</p><h3 id="Hardware-acceleration"><a href="#Hardware-acceleration" class="headerlink" title="Hardware acceleration"></a>Hardware acceleration</h3><p>A model inference can have a much higher performance by leveraging a GPU, but since it’s highly platform-dependent (especially when you put Docker in the mix), it was intentionally disabled. Moreover, depending on your requirements, your application could also benefit from a <a href="https://gpu.rocks/">hardware-accelerated canvas</a>. And even without a discrete GPU, you could still optimize the performance by using the right CPU instructions or CPU architecture that your ML framework is optimized for.</p><h3 id="Vector-database"><a href="#Vector-database" class="headerlink" title="Vector database"></a>Vector database</h3><p>This demo application stores face data in memory. In a production application, you would likely do that using persistent storage, such as a database. One of the most widely used solutions is Postgres with the pg_vector extension, which enables efficient indexing and querying of N-dimensional vector columns. There are also specialized databases, such as Pinecone or Qdrant, which, depending on the specific needs, could be a good choice for a vector store and a retrieval engine.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Computer vision in general and facial recognition in particular are rapidly growing fields of computer science that can be beneficial in or even revolutionize some areas, such as transportation or retail. In this article we’re going to look closely at how such system could be built.&lt;/p&gt;</summary>
    
    
    
    
    <category term="Machine Learning" scheme="https://devalent.com/blog/tags/Machine-Learning/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
  </entry>
  
  <entry>
    <title>AWS Lambda ARM64 vs x64 Benchmarks</title>
    <link href="https://devalent.com/blog/aws-lambda-arm64-vs-x64-benchmarks/"/>
    <id>https://devalent.com/blog/aws-lambda-arm64-vs-x64-benchmarks/</id>
    <published>2021-12-24T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.379Z</updated>
    
    <content type="html"><![CDATA[<p>Amazon has recently released <a href="https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/">ARM64 support for Lambda</a> based on Graviton2 processors. They claim that the new architecture provides better price performance. Let’s take a look how it stands up against the classic x64 CPU in some real-world scenarios.</p><span id="more"></span><h1 id="Test-Environment"><a href="#Test-Environment" class="headerlink" title="Test Environment"></a>Test Environment</h1><p>We’re going to use Node.js 14 inside Docker as the runtime for test functions of various sizes: from 128 MB to 10240 MB, each one twice as big as the previous. The infrastructure will be managed with <a href="https://www.pulumi.com/">Pulumi</a>. The <a href="https://github.com/Devalent/lambda-benchmarks">source code</a> is available on GitHub.</p><figure class="codeblock codeblock--tabbed"><figcaption><span>lambda.ts</span><ul class="tabs"><li class="tab active">typescript</li></ul></figcaption><div class="tabs-content"><figure class="highlight typescript" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> arch <span class="keyword">of</span> [<span class="string">'x86_64'</span>, <span class="string">'arm64'</span>]) {</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">7</span>; i &lt;= <span class="number">14</span>; i++) {</span><br><span class="line">    <span class="keyword">const</span> size = <span class="built_in">Math</span>.min(<span class="number">2</span> ** i, <span class="number">10240</span>);</span><br><span class="line">    <span class="keyword">const</span> name = <span class="string">`<span class="subst">${project}</span>-<span class="subst">${stack}</span>-<span class="subst">${arch}</span>-<span class="subst">${size}</span>`</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">new</span> aws.lambda.Function(name, {</span><br><span class="line">      <span class="attr">name</span>: name,</span><br><span class="line">      <span class="attr">architectures</span>: arch,</span><br><span class="line">      <span class="attr">memorySize</span>: size,</span><br><span class="line">      <span class="attr">timeout</span>: <span class="number">60</span>,</span><br><span class="line">      <span class="attr">role</span>: role.arn,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">`<span class="subst">${project}</span>-<span class="subst">${stack}</span> - Worker (<span class="subst">${arch}</span> <span class="subst">${size}</span> MB).`</span>,</span><br><span class="line">      <span class="attr">imageUri</span>: pulumi.interpolate<span class="string">`<span class="subst">${account}</span>.dkr.ecr.<span class="subst">${region}</span>.amazonaws.com/<span class="subst">${repository.name}</span>:latest`</span>,</span><br><span class="line">      <span class="attr">packageType</span>: <span class="string">'Image'</span>,</span><br><span class="line">    });</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure></div></figure><p>All function will run benchmarks that last 100 seconds and executed twice: the first run is intended to warm up the environment, the second provides the final results.</p><h1 id="Express-js-Benchmark"><a href="#Express-js-Benchmark" class="headerlink" title="Express.js Benchmark"></a>Express.js Benchmark</h1><p>The first benchmark uses a simple <a href="https://www.npmjs.com/package/express">Express.js</a> server that handles requests. The server is invoked from within the same function.</p><figure class="codeblock codeblock--tabbed"><figcaption><span>function.ts</span><ul class="tabs"><li class="tab active">typescript</li></ul></figcaption><div class="tabs-content"><figure class="highlight typescript" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> app = express();</span><br><span class="line"></span><br><span class="line">app.post(<span class="string">'/'</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> {</span><br><span class="line">  res.json(req.body);</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> server = app.listen();</span><br><span class="line"></span><br><span class="line">benny.suite(<span class="string">'Lambda benchmark'</span>, </span><br><span class="line">  benny.add(<span class="string">'express'</span>, <span class="function">() =&gt;</span> {</span><br><span class="line">    <span class="keyword">const</span> runner = <span class="keyword">async</span> () =&gt; {</span><br><span class="line">      <span class="keyword">await</span> axios.post(<span class="string">`http://localhost:<span class="subst">${server.address()![<span class="string">'port'</span>]}</span>/`</span>, {});</span><br><span class="line">    };</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> runner;</span><br><span class="line">  }, {</span><br><span class="line">    <span class="attr">maxTime</span>: <span class="number">10</span>,</span><br><span class="line">  }),</span><br><span class="line">  benny.cycle(),</span><br><span class="line">);</span><br></pre></td></tr></tbody></table></figure></div></figure><p>The results are quite interesting:</p><div style="width: 100;margin: 0 auto">    <canvas id="chart7374" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart7374').getContext('2d');    var options =  {  type: 'line',  data: {    labels: [128,256,512,1024,2048,4096,8192,10240],    datasets: [      {         data: [47,130,343,881,1551,1648,1585,1510],        label: "x64",        borderColor: "#3e95cd",        fill: false      },      {         data: [39,127,331,713,1393,1257,1342,1279],        label: "ARM64",        borderColor: "#8e5ea2",        fill: false      }    ]  },  options: {    legend: {      labels: {        fontColor: "#fff"      }    },    title: {      fontColor: "#fff",      display: true,      text: "Express.js requests per second"    },    grid: {      color: "white"    },    scales: {      xAxes: [{        ticks: {          min: 0,          fontColor: "#f4f3f3"        }      }],      yAxes: [{        ticks: {          fontColor: "#f4f3f3"        }      }]    }  }};;    new Chart(ctx, options);</script><p>As you can see, both x64 and ARM64 have a similar performance in functions up to 512 MB. As you might know, Lambda’s CPU performance and core count scales up alongside the allocated RAM. Both architectures peak at around 2048 MB, with x64 leading the race. The performance starts to decline after 4096 MB, which could be an indication that high-memory functions have more CPU cores in exchange for a lower performance per core. Here’s the distribution of the number of CPU cores:</p><div style="width: 100;margin: 0 auto">    <canvas id="chart2408" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart2408').getContext('2d');    var options =  {  type: 'bar',  data: {    labels: [128,256,512,1024,2048,4096,8192,10240],    datasets: [      {        label: "CPU cores",        backgroundColor: "#3e95cd",        data: [2,2,2,2,2,3,5,6]      }    ]  },  options: {    legend: { display: false },    title: {      fontColor: "#fff",      display: true,      text: "CPU core count"    },    grid: {      color: "white"    },    scales: {      xAxes: [{        ticks: {          min: 0,          fontColor: "#f4f3f3"        }      }],      yAxes: [{        ticks: {          min: 0,          stepSize: 1,          fontColor: "#f4f3f3"        }      }]    }  }};;    new Chart(ctx, options);</script><p>Because this benchmark does not rely on asynchronous code or clustering, there is no gain from extra CPU cores.</p><p>Since ARM64 has a lower price than x64, let’s compare the two in terms of price performance:</p><div style="width: 100;margin: 0 auto">    <canvas id="chart121" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart121').getContext('2d');    var options =  {  type: 'line',  data: {    labels: [128,256,512,1024,2048,4096,8192,10240],    datasets: [      {         data: [44.7,32.3,24.2,19,21.5,40.5,84.1,110.4],        label: "x64",        borderColor: "#3e95cd",        fill: false      },      {         data: [43.6,26.8,20.2,18.7,19.2,42.4,79.5,104.2],        label: "ARM64",        borderColor: "#8e5ea2",        fill: false      }    ]  },  options: {    legend: {      labels: {        fontColor: "#fff"      }    },    title: {      fontColor: "#fff",      display: true,      text: "Price per 1 billion requests ($)"    },    grid: {      color: "white"    },    scales: {      xAxes: [{        ticks: {          min: 0,          fontColor: "#f4f3f3"        }      }],      yAxes: [{        ticks: {          fontColor: "#f4f3f3"        }      }]    }  }};;    new Chart(ctx, options);</script><p>Just as expected, ARM64 has up to a 20% lower cost per operation than x64. The largest gap is within 256 and 512 MB range, which is often used for APIs and other Express.js scenarios, so it’s worth checking out if your Lambda web services can be migrated to ARM64 to get a cost reduce.</p><h1 id="Native-Code-Benchmark"><a href="#Native-Code-Benchmark" class="headerlink" title="Native Code Benchmark"></a>Native Code Benchmark</h1><p>In the second benchmark we’re going to use <a href="https://www.npmjs.com/package/sharp">Sharp.js</a> - a Node.js binding to image processing library <a href="https://github.com/libvips/libvips">libvips</a>, which is available for both x64 and ARM64. The benchmark scales down a 1280x800 image to 640x400 and saves it as a JPEG file.</p><figure class="codeblock codeblock--tabbed"><figcaption><span>function.ts</span><ul class="tabs"><li class="tab active">typescript</li></ul></figcaption><div class="tabs-content"><figure class="highlight typescript" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">sharp.cache(<span class="literal">false</span>);</span><br><span class="line">sharp.concurrency(os.cpus().length);</span><br><span class="line"></span><br><span class="line">benny.suite(<span class="string">'Lambda benchmark'</span>, </span><br><span class="line">  benny.add(<span class="string">'sharp'</span>, <span class="function">() =&gt;</span> {</span><br><span class="line">    <span class="keyword">const</span> runner = <span class="keyword">async</span> () =&gt; {</span><br><span class="line">      <span class="keyword">await</span> sharp(<span class="string">'./image.jpeg'</span>)</span><br><span class="line">        .resize(<span class="number">640</span>, <span class="number">400</span>)</span><br><span class="line">        .jpeg({ <span class="attr">quality</span>: <span class="number">80</span> })</span><br><span class="line">        .toBuffer();</span><br><span class="line">    };</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> runner;</span><br><span class="line">  }, {</span><br><span class="line">    <span class="attr">maxTime</span>: <span class="number">10</span>,</span><br><span class="line">  }),</span><br><span class="line">  benny.cycle(),</span><br><span class="line">);</span><br></pre></td></tr></tbody></table></figure></div></figure><p>The results of this benchmark are unexpected:</p><div style="width: 100;margin: 0 auto">    <canvas id="chart4782" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart4782').getContext('2d');    var options =  {  type: 'line',  data: {    labels: [256,512,1024,2048,4096,8192,10240],    datasets: [      {         data: [3,5,12,22,28,30,29],        label: "x64",        borderColor: "#3e95cd",        fill: false      },      {         data: [3,5,12,22,29,32,32],        label: "ARM64",        borderColor: "#8e5ea2",        fill: false      }    ]  },  options: {    legend: {      labels: {        fontColor: "#fff"      }    },    title: {      fontColor: "#fff",      display: true,      text: "Sharp.js image operations per second"    },    grid: {      color: "white"    },    scales: {      xAxes: [{        ticks: {          min: 0,          fontColor: "#f4f3f3"        }      }],      yAxes: [{        ticks: {          fontColor: "#f4f3f3"        }      }]    }  }};;    new Chart(ctx, options);</script><p>Both CPUs perform on on par with each other up until 2048 MB, after which ARM64 breaks ahead. This could be just a result of a better performance of libvips on ARM64. Let’s see what is the price of the two:</p><div style="width: 100;margin: 0 auto">    <canvas id="chart2397" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart2397').getContext('2d');    var options =  {  type: 'line',  data: {    labels: [256,512,1024,2048,4096,8192,10240],    datasets: [      {         data: [140,166,139.2,151.4,238.2,444.3,574.8],        label: "x64",        borderColor: "#3e95cd",        fill: false      },      {         data: [113.3,134,110.8,121.4,183.8,333.4,416.6],        label: "ARM64",        borderColor: "#8e5ea2",        fill: false      }    ]  },  options: {    legend: {      labels: {        fontColor: "#fff"      }    },    title: {      fontColor: "#fff",      display: true,      text: "Price per 1 million requests ($)"    },    grid: {      color: "white"    },    scales: {      xAxes: [{        ticks: {          min: 0,          fontColor: "#f4f3f3"        }      }],      yAxes: [{        ticks: {          fontColor: "#f4f3f3"        }      }]    }  }};;    new Chart(ctx, options);</script><p>The difference in performance price is more profound in this benchmark, with ARM64 being up to 27% less expensive than x64. This makes ARM64 an attractive alternative for computationally intensive functions.</p><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Migrating your Lambda functions from x64 to ARM64 can give you a noticeable improvement in price performance. Of course, your mileage may vary, so you should consider your own research before making any assumptions.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Amazon has recently released &lt;a href=&quot;https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/&quot;&gt;ARM64 support for Lambda&lt;/a&gt; based on Graviton2 processors. They claim that the new architecture provides better price performance. Let’s take a look how it stands up against the classic x64 CPU in some real-world scenarios.&lt;/p&gt;</summary>
    
    
    
    
    <category term="Cloud Computing" scheme="https://devalent.com/blog/tags/Cloud-Computing/"/>
    
    <category term="Serverless" scheme="https://devalent.com/blog/tags/Serverless/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
  </entry>
  
  <entry>
    <title>Production AWS Lambda Performance Checklist</title>
    <link href="https://devalent.com/blog/production-aws-lambda-performance-checklist/"/>
    <id>https://devalent.com/blog/production-aws-lambda-performance-checklist/</id>
    <published>2021-11-12T00:00:00.000Z</published>
    <updated>2025-04-06T17:19:46.404Z</updated>
    
    <content type="html"><![CDATA[<p>AWS Lambda is a mature, feature-rich computing platform. While it’s very straightforward and simple to use for backend developers, when it comes to the performance tuning, there are a few things to keep in mind.</p><span id="more"></span><h1 id="Startup-Performance"><a href="#Startup-Performance" class="headerlink" title="Startup Performance"></a>Startup Performance</h1><p>The cornerstone of Lambda performance is startup time. Whenever a Lambda function is invoked for the first time, it spins up a function instance and goes through the runtime initialization. This process can take a noticeable amount of time which is often unacceptable in production. A new Lambda instance can also get created during increased usage, parallel invocations or after a function has been idle for a while. To mitigate this issue, it is possible to pre-warm a Lambda function, so that it will already be initialized by the time the first request will come in. There are currently two similar approaches to choose from.</p><h2 id="WarmUp-Plugin"><a href="#WarmUp-Plugin" class="headerlink" title="WarmUp Plugin"></a>WarmUp Plugin</h2><p>If you are using the <a href="https://www.serverless.com/">Serverless Framework</a>, you can implement function pre-warm using the <a href="https://www.npmjs.com/package/serverless-plugin-warmup">Serverless WarmUp Plugin</a>. It works by invoking your Lambda functions from a “warmer” function on a specified schedule (say, every five minutes) to simulate a user request or an event. You can also configure a concurrency at which your function will be invoked, which effectively provisions multiple function instances.</p><p>It is also possible to provide additional function initialization code, for example to connect to a database or instantiate any resource-heavy code:</p><figure class="codeblock codeblock--tabbed"><figcaption><span>index.ts</span><ul class="tabs"><li class="tab active">typescript</li></ul></figcaption><div class="tabs-content"><figure class="highlight typescript" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Lambda handler</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> handler = <span class="keyword">async</span> (event, context) =&gt; {</span><br><span class="line">  <span class="keyword">if</span> (event.source === <span class="string">'serverless-plugin-warmup'</span>) {</span><br><span class="line">    <span class="comment">// Lambda initialization code.</span></span><br><span class="line">    <span class="keyword">await</span> connect();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Regular code</span></span><br><span class="line">  <span class="keyword">const</span> db = <span class="keyword">await</span> connect();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> db.query();</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// Database connection code</span></span><br><span class="line"><span class="keyword">let</span> dbClient;</span><br><span class="line"><span class="keyword">let</span> dbClientTask:<span class="built_in">Promise</span>&lt;<span class="built_in">any</span>&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">connect</span>(<span class="params"></span>) </span>{</span><br><span class="line">  <span class="keyword">if</span> (dbClient) {</span><br><span class="line">    <span class="keyword">return</span> dbClient;</span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!dbClientTask) {</span><br><span class="line">    <span class="keyword">const</span> task = <span class="keyword">async</span> () =&gt; {</span><br><span class="line">      dbClient = <span class="keyword">new</span> DbClient();</span><br><span class="line"></span><br><span class="line">      <span class="keyword">await</span> dbClient.connect();</span><br><span class="line">    };</span><br><span class="line"></span><br><span class="line">    dbClientTask = task();</span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> {</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> dbClientTask;</span><br><span class="line">  } <span class="keyword">finally</span> {</span><br><span class="line">    dbClientTask = <span class="literal">undefined</span>;</span><br><span class="line">  }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure></div></figure><p>Note that it is not guaranteed that the initialization code will be invoked before an actual request is made or that it will be invoked at all, so make sure that your initialization logic is lazy is idempotent.</p><p>Keep in mind that the warmup process will add up to your Lambda bill just like any other Lambda request. A single function configured to be invoked every five minutes will account for 8640 calls each month. Depending on your requirements, it could be benifitial to only pre-warm your function during business hours. You can do so by providing a CRON expression to your warmup configuration:</p><figure class="codeblock codeblock--tabbed"><figcaption><span>serverless.yaml</span><ul class="tabs"><li class="tab active">yaml</li></ul></figcaption><div class="tabs-content"><figure class="highlight yaml" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">custom:</span></span><br><span class="line">  <span class="attr">warmup:</span></span><br><span class="line">    <span class="attr">default:</span></span><br><span class="line">      <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">events:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">schedule:</span> <span class="string">"cron(0/5 8-17 ? * MON-FRI *)"</span></span><br></pre></td></tr></tbody></table></figure></div></figure><h2 id="Provisioned-concurrency"><a href="#Provisioned-concurrency" class="headerlink" title="Provisioned concurrency"></a>Provisioned concurrency</h2><p>AWS <a href="https://aws.amazon.com/blogs/aws/new-provisioned-concurrency-for-lambda-functions/">introduced</a> a native way to pre-warm Lambda functions called “provisioned concurrency”. You just need to provide a number of function instances you want to keep warm and AWS will take care of the rest. If you are using the Serverless framework, you can configure provisioned concurrency in the following way:</p><figure class="codeblock codeblock--tabbed"><figcaption><span>serverless.yaml</span><ul class="tabs"><li class="tab active">yaml</li></ul></figcaption><div class="tabs-content"><figure class="highlight yaml" style="display: block;"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">functions:</span></span><br><span class="line">  <span class="attr">func:</span></span><br><span class="line">    <span class="attr">provisionedConcurrency:</span> <span class="number">2</span></span><br></pre></td></tr></tbody></table></figure></div></figure><p>The way pre-warmed functions get initialized differs from the WarmUp plugin. With provisioned concurrency, the handler function is not called upon initialization. This results in a drawback that prevents from running an asynchronous initialization code in Node.js. In order to get around this limitation, you can use a <a href="https://github.com/chuckbarkertech/nodejs-example-async-init-extension">Lambda layer</a> that allows to provide a callback during the initialization.</p><p>One of the benefits of this being a native feature is that it is possible to <a href="https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html#managing-provisioned-concurency">automatically scale a provisioned concurrency</a> for a function based on its utilization or on a schedule with scaling policies. </p><p>Whenever a provisioned concurrency is configured for a function, it will get a discount on the execution duration cost, but will also get an additional fee as long as this feature is active. See <a href="https://aws.amazon.com/lambda/pricing/">AWS Lambda pricing</a> for the details.</p><h2 id="Code-Optimization"><a href="#Code-Optimization" class="headerlink" title="Code Optimization"></a>Code Optimization</h2><p>The package size of a Lambda function affects the time it takes to initialize it. There is also an account-level limit on the total size of all Lambda functions, so it is advisable to keep the size of your functions in check. This is especially relevant in case of Node.js functions with lots of npm dependencies.</p><p>By default, npm dependencies get packaged with a function code as-is, with all their contents intact. Many packages include things like documentation or even media files that are not needed in order to run these packages. A possible solution to this issue is to use a code bundler like Webpack (or better yet, <a href="https://www.npmjs.com/package/serverless-bundle">Serverless Bundle</a>) to transpile dependencies into a single output file. The resulting package can easily be multiple times smaller than the same function with included <code>node_modules</code> directory. </p><div style="width: 100;margin: 0 auto">    <canvas id="chart4756" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart4756').getContext('2d');    var options =  {  type: 'horizontalBar',  data: {    labels: ["With node_modules", "Transpiled with serverless-bundle"],    datasets: [      {        label: "Lambda function size",        backgroundColor: ["#3e95cd", "#8e5ea2"],        data: [3.6, 1.3]      }    ]  },  options: {    legend: { display: false },    title: {      fontColor: "#fff",      display: true,      text: "Lambda function size with Express.js as a dependency (MB)"    },    grid: {      color: "white"    },    scales: {      xAxes: [{          ticks: {              min: 0,              fontColor: "#f4f3f3"          }      }],      yAxes: [{          ticks: {              fontColor: "#f4f3f3"          }      }]    }  }};;    new Chart(ctx, options);</script><h1 id="Connectivity"><a href="#Connectivity" class="headerlink" title="Connectivity"></a>Connectivity</h1><p>Another important topic related to Lambda performance is network connectivity. Lambda function instances can be created or teared down at any time, execution environment can be freezed in between requests. All of that can affect persistent network connections and increase reconnection rate, resulting in subpar application performance.</p><h2 id="AWS-SDK"><a href="#AWS-SDK" class="headerlink" title="AWS SDK"></a>AWS SDK</h2><p>If you’re still using AWS SDK v2 for Node.js, there is a single-line change that can greatly increase its performance. Just add <code>AWS_NODEJS_CONNECTION_REUSE_ENABLED=1</code> as an environment variable of your function. As the name suggests, it enables a reuse of TCP connections, so subsequent requests to AWS API will take noticeable less time. AWS SDK v3 has this behavior enabled by default.</p><div style="width: 100;margin: 0 auto">    <canvas id="chart7734" style="height: %px"></canvas></div><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script><script type="text/javascript">    var ctx = document.getElementById('chart7734').getContext('2d');    var options =  {  type: 'line',  data: {    labels: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],    datasets: [      {         data: [79.929, 80.46, 59.901, 58.348, 94.834, 60.966, 65.364, 79.781, 57.84, 61.739, 78.874, 56.345, 48.839, 71.575, 40.834, 65.233, 58.205, 109.74, 65.285, 59.147],        label: "No connection reuse",        borderColor: "#3e95cd",        fill: false      },      {         data: [27.614, 39.923, 19.767, 20.525, 19.876, 40.193, 20.172, 27.12, 16.322, 23.351, 20.557, 19.912, 39.891, 19.904, 19.907, 40.494, 39.346, 19.831, 19.915, 39.903],        label: "With connection reuse",        borderColor: "#8e5ea2",        fill: false      }    ]  },  options: {    legend: {      labels: {        fontColor: "#fff"      }    },    title: {      fontColor: "#fff",      display: true,      text: "Subsequent AWS API calls duration (milliseconds)"    },    grid: {      color: "white"    },    scales: {      xAxes: [{          ticks: {              min: 0,              fontColor: "#f4f3f3"          }      }],      yAxes: [{          ticks: {              fontColor: "#f4f3f3"          }      }]    }  }};;    new Chart(ctx, options);</script><h2 id="Database-Proxying"><a href="#Database-Proxying" class="headerlink" title="Database Proxying"></a>Database Proxying</h2><p>The best practice when using a remote database in Lambda is to use a proxy, either a stateless API (like DynamoDB) or a connection pool. Since Lambda can scale up to multiple instances really quick and can dispose an existing one at any time, it can result in too many connections to a database server. <a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-database.html">AWS RDS Proxy</a> is a serverless solution that provides connection pooling for RDS databases. If you don’t use RDS, there are similar products for most of the popular databases, such as PgBouncer for PostgreSQL.</p><h1 id="Monitoring-amp-Profiling"><a href="#Monitoring-amp-Profiling" class="headerlink" title="Monitoring &amp; Profiling"></a>Monitoring &amp; Profiling</h1><p>It is important to understand the performance bottlenecks of your Lambda functions when using it for mission-critical workflows and in high-load projects. With serverless approach, you can’t rely on a toolset that exists in the world of classic servers, but there are a few AWS products that can help you to get the job done.</p><h2 id="Lambda-Insights"><a href="#Lambda-Insights" class="headerlink" title="Lambda Insights"></a>Lambda Insights</h2><p>By defaut, Lambda provides only a handful of performance metrics, such as execution duration and concurrency. If you want to get more information on what’s actually hapenning in your functions, you can enable <a href="https://docs.aws.amazon.com/lambda/latest/dg/monitoring-insights.html">Lambda Insights</a>. With that in place, you will get detailed metrics for CPU, RAM and IO usage, as well as the information on cold function starts. This data is crucial for fine-tuning your function memory size, especially if you peform computationally-intensive tasks in Lambda.</p><h2 id="X-Ray"><a href="#X-Ray" class="headerlink" title="X-Ray"></a>X-Ray</h2><p>Last but not least is tracing service for your distributed applications: <a href="https://docs.aws.amazon.com/lambda/latest/dg/nodejs-tracing.html">AWS X-Ray</a>. If you lerevage a microservice approach, you absolutely need to monitor how your requests are propagated through the system to identify any otherwise hard to pinpoint performance issues that can arise. With just a few lines of code, you can break down your requests into sequence diagrams and see which parts of your system needs an attention. </p><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>With just a few easy steps you can make you Lambda functions much faster without even changing the architecture.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;AWS Lambda is a mature, feature-rich computing platform. While it’s very straightforward and simple to use for backend developers, when it comes to the performance tuning, there are a few things to keep in mind.&lt;/p&gt;</summary>
    
    
    
    
    <category term="Cloud Computing" scheme="https://devalent.com/blog/tags/Cloud-Computing/"/>
    
    <category term="Serverless" scheme="https://devalent.com/blog/tags/Serverless/"/>
    
    <category term="Artem Abashev" scheme="https://devalent.com/blog/tags/Artem-Abashev/"/>
    
  </entry>
  
</feed>
