<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Meadowrun</title>
    <description>The latest articles on DEV Community by Meadowrun (@meadowrun).</description>
    <link>https://dev.to/meadowrun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5750%2F0de0fb20-aa1e-45e6-9a40-3b66d7e4b6e5.png</url>
      <title>DEV Community: Meadowrun</title>
      <link>https://dev.to/meadowrun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meadowrun"/>
    <language>en</language>
    <item>
      <title>Running pytest in the Cloud for Fun and Profit</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Wed, 07 Sep 2022 14:06:14 +0000</pubDate>
      <link>https://dev.to/meadowrun/running-pytest-in-the-cloud-for-fun-and-profit-ldd</link>
      <guid>https://dev.to/meadowrun/running-pytest-in-the-cloud-for-fun-and-profit-ldd</guid>
      <description>&lt;h4&gt;
  
  
  Introducing pytest-cloudist on Meadowrun
&lt;/h4&gt;

&lt;p&gt;Nobody likes waiting, but it seems to be part of the developer life: we wait for builds, tests, code review, and deployments. This is especially annoying given that computers are more powerful than ever and the cloud promises access to infinite compute resources.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ghf1BYFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AUJqhABJcR-7UQ3-CzTUSGg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ghf1BYFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AUJqhABJcR-7UQ3-CzTUSGg.png" alt="" width="880" height="880"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A python in the clouds. Generated by Stable Diffusion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post, we introduce &lt;a href="https://github.com/kurtschelfthout/pytest-cloudist"&gt;pytest-cloudist&lt;/a&gt;, a plugin for the Python testing library p&lt;a href="https://pytest.org"&gt;ytest&lt;/a&gt;. pytest-cloudist leverages &lt;a href="https://meadowrun.io"&gt;Meadowrun&lt;/a&gt; to run pytest on any number of cloud machines with a minimal amount of fuss. The name is a riff on the venerable &lt;a href="https://pytest-xdist.readthedocs.io/en/latest/"&gt;pytest-xdist&lt;/a&gt;, which is mainly used for running tests locally in parallel. pytest-xdist does support distributed runs using SSH, but pytest-cloudist develops this capability further by provisioning cloud compute on-demand and synchronizing our code and libraries across machines seamlessly.&lt;/p&gt;

&lt;p&gt;We’ll introduce pytest-cloudist through a case study of running the &lt;a href="https://pandas.pydata.org/"&gt;pandas&lt;/a&gt; unit tests on AWS EC2 virtual machines.&lt;/p&gt;
&lt;h3&gt;
  
  
  Running the pandas tests locally
&lt;/h3&gt;

&lt;p&gt;First, we’ll get the pandas tests running locally to establish a baseline. For all of the local runs, we’ll be using a laptop which has an Intel Core i7 with 12 vCPUs and 16 GiB RAM.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pandas.pydata.org/pandas-docs/stable/development/contributing_environment.html#id5"&gt;development documentation&lt;/a&gt; has a few well-documented options to set up a pandas development environment. We went with a low-tech option, a virtualenv with libraries installed by pip. The &lt;a href="https://github.com/pandas-dev/pandas/blob/main/requirements-dev.txt"&gt;requirements-dev.txt&lt;/a&gt; in the pandas repo is quite large, so we trimmed &lt;a href="https://gist.github.com/kurtschelfthout/d4dbe132771cf96d6b401eddfacfa33a#file-installed-dependencies-txt"&gt;it down to the essentials&lt;/a&gt; which means we’ll skip a few of the integration tests involving other packages like geopandas.&lt;/p&gt;

&lt;p&gt;We’ll also skip tests marked as slow, network, or db, following the logic in &lt;a href="https://github.com/pandas-dev/pandas/blob/main/test_fast.sh#L8"&gt;test_fast.sh&lt;/a&gt;, and we also explicitly disabled a handful of &lt;a href="https://gist.github.com/kurtschelfthout/d4dbe132771cf96d6b401eddfacfa33a#file-disabled-txt"&gt;flaky tests&lt;/a&gt;. This still leaves over 100,000 tests to run.&lt;/p&gt;
&lt;h4&gt;
  
  
  Running vanilla pytest
&lt;/h4&gt;

&lt;p&gt;One last bit of configuration. We used a &lt;a href="https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_report_teststatus"&gt;pytest hook&lt;/a&gt; to &lt;a href="https://gist.github.com/kurtschelfthout/d4dbe132771cf96d6b401eddfacfa33a#file-conftest-py"&gt;turn off printing dots to the terminal&lt;/a&gt; for each completed test. This was adding a minute to the total runtime and wasn’t providing much value for running more than 100,000 tests. (Don’t get me started on how my laptop runs Fortnite at 60fps but printing dots to the terminal takes ages.)&lt;/p&gt;

&lt;p&gt;On my laptop, running pytest on the pandas tests takes about 13.5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; time pytest --skip-slow --skip-network --skip-db -m "not single_cpu" pandas

============= 145386 passed, 21573 skipped, 1230 xfailed, 1 xpassed, 22 warnings in 810.80s (0:13:30) ==============

real 13m34.698s
user 12m25.510s
sys 0m22.408s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Running in parallel with pytest-xdist
&lt;/h4&gt;

&lt;p&gt;Pandas recommends running pytest-xdist with 4 workers in &lt;a href="https://github.com/pandas-dev/pandas/blob/main/test_fast.sh#L8"&gt;test_fast.sh&lt;/a&gt;, but on my laptop, that actually results in a slowdown because the worker processes run out of memory. I did manage to get good results with 2 worker processes though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; time pytest --skip-slow --skip-network --skip-db -m "not single_cpu" -n 2 pandas

============= 145386 passed, 21573 skipped, 1230 xfailed, 1 xpassed, 22 warnings in 447.80s (0:07:27) ==============

real 7m32.799s
user 16m18.856s
sys 0m56.269s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice — almost a 2x speedup.&lt;/p&gt;

&lt;h4&gt;
  
  
  Amdahl’s Law
&lt;/h4&gt;

&lt;p&gt;Why don’t we get the full 2x speedup, i.e. just under 7 minutes? Because of &lt;a href="https://www.wikiwand.com/en/Amdahl%27s_law"&gt;Amdahl’s law&lt;/a&gt; — not all the work is parallelizable.&lt;/p&gt;

&lt;p&gt;First, pytest collects tests to run on a single thread, which takes over a minute for the pandas tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time pytest --skip-slow --skip-network --skip-db -m "not single_cpu" --collect-only pandas

=== 168158/169338 tests collected (1180 deselected) in 72.39s (0:01:12) ===

real 1m19.560s
user 0m59.860s
sys 0m7.425s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s also time spent aggregating the results as tests complete, which is harder to isolate and measure.&lt;/p&gt;

&lt;p&gt;Back of the envelope, we have 13.5 minutes of work total, of which 1 minute is test collection time, and about another minute is test aggregation and reporting time. That leaves 11.5 minutes of embarrassingly parallel work, i.e. running the tests, which lines up with the roughly 7.5 minute runtime (2 + 11.5 / 2) that we see in practice for two workers.&lt;/p&gt;

&lt;p&gt;I’d like to try more workers, but sadly, two is close to what my laptop can handle — I had trouble doing anything else while the test was running as it consumed almost all of my laptop’s memory.&lt;/p&gt;

&lt;p&gt;The current state of the art then, puts me between a rock and a hard place: I can run tests slowly sequentially and use my laptop for something else, or I can run tests quickly in parallel but make my laptop unusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running pandas’ tests in the cloud
&lt;/h3&gt;

&lt;p&gt;With pytest-cloudist, there’s now a third option: I can run the tests in parallel on AWS spot instances. Pytest-cloudist is a fairly thin wrapper around Meadowrun, which does the heavy lifting of creating EC2 instances, starting workers and deploying the environment and local code. In theory this is a seamless experience (Meadowrun maintainer bias warning!).&lt;/p&gt;

&lt;h4&gt;
  
  
  Getting started with pytest-cloudist
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/kurtschelfthout/pytest-cloudist#installation"&gt;pytest-cloudist installation&lt;/a&gt; is like any other pytest plugin: you install it using pip or poetry alongside pytest, and it’s automagically available. If you haven’t set up Meadowrun in your AWS account before, there’s &lt;a href="https://docs.meadowrun.io/en/stable/tutorial/install/"&gt;an additional step&lt;/a&gt; which we won’t repeat here.&lt;/p&gt;

&lt;p&gt;By default cloud distribution is not enabled; it only kicks in if you pass the &lt;code&gt;--cloudist test&lt;/code&gt; or &lt;code&gt;--cloudist file&lt;/code&gt; arguments to pytest. The former distributes each individual test as a separate task and the latter distributes each test file as a task. Since pandas has over 100,000 tests, making tests into individual tasks introduces way too much overhead, so we’ll use per-file distribution exclusively in this post. There are about 850 test files, so still plenty of parallelization opportunity.&lt;/p&gt;

&lt;p&gt;Further pytest-cloudist options all start with &lt;code&gt;--cd&lt;/code&gt;. There are options to control the number of workers and how much CPU and memory each worker needs. This information is passed straight to Meadowrun which takes care of creating machines of the right size.&lt;/p&gt;

&lt;p&gt;There’s also an option &lt;code&gt;--cd-tasks-per-worker-target&lt;/code&gt; for combining tests or files into bigger tasks to maximize performance. Workers invoke pytest once per task, but there is some overhead for each invocation of pytest. To reduce this overhead, we can ask cloudist to combine multiple files into tasks so each task runs multiple files in one go. For example, if there are 850 test files, a run with &lt;code&gt;--cloudist file --cd-num-workers 4 --cd-tasks-per-worker-target 10&lt;/code&gt; tries to create 40 tasks total (4 workers*10 tasks/worker). Each task consists of roughly 21 test files each (850 files//40 tasks).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--cd-tasks-per-worker-target 1&lt;/code&gt; minimizes the pytest invocation overhead, but also introduces a problem: a single slow task could take significantly longer than the other tasks, resulting in a longer overall runtime. We find empirically that a tasks per worker target between 5 and 20 works well.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuration for the pandas tests
&lt;/h4&gt;

&lt;p&gt;The first issue we ran into when trying to run the pandas tests using pytest-cloudist is that pandas has Cython dependencies which Meadowrun currently doesn’t take care of automatically. To solve this, we compiled locally and then used cloudist’s &lt;code&gt;--cd-extra-files&lt;/code&gt; argument to sync the .so and .pxd files to the remote machines. The &lt;code&gt;--cd-extra-files&lt;/code&gt; argument is similar to pytest-xdist’s &lt;code&gt;--rsyncdir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second issue is that some of the tests rely on data files. We can use the same &lt;code&gt;--cd-extra-files&lt;/code&gt; argument to make sure these files are copied to the remote machines.&lt;/p&gt;

&lt;p&gt;Here’s the full command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time pytest 
--skip-slow --skip-network --skip-db -m "not single_cpu" 
--cloudist file
--cd-extra-files 'pandas/_libs/**/*.pxd'
--cd-extra-files 'pandas/_libs/**/*.so' 
--cd-extra-files 'pandas/io/sas/*.so'
--cd-extra-files 'pandas/tests/ **/data/**' 
--cd-num-workers 2
--cd-cpu-per-worker 2
--cd-memory-per-worker 6 
--cd-tasks-per-worker-target 20 
pandas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we didn’t have to specify explicitly which python files to make available or what the environment should be. pytest-cloudist, via Meadowrun, figures that out by itself.&lt;/p&gt;

&lt;p&gt;Also, we’ve given each worker two CPUs instead of the default of one as this seems to benefit some of the tests.&lt;/p&gt;

&lt;h4&gt;
  
  
  We have lift off
&lt;/h4&gt;

&lt;p&gt;When starting a run, pytest collects tests as normal, but then pytest-cloudist (or Meadowrun, really) kicks in to create the necessary EC2 instances and synchronize the current Python environment, code, and extra files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mirroring current pip environment
0/2 workers allocated to existing instances: 
The current run_map's id is 6610ce7a-08c6-4995-8a29-59e3c66dac68
Launched 1 new instance(s) (total $0.0411/hr) for the remaining 2 workers:
        ec2-18-219-20-201.us-east-2.compute.amazonaws.com: m5.xlarge (4.0 CPU, 16.0 GB), spot ($0.0411/hr, 2.5% eviction rate), will run 2 workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, Meadowrun detected that we’re running in a pip virtual environment. Meadowrun recreates virtual environments on the remote machine by building a container. This can take some time, but the resulting container is cached in &lt;a href="https://aws.amazon.com/ecr/"&gt;ECR&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then, local code and extra files are zipped and uploaded to S3. The file is not re-uploaded if its contents hasn’t changed.&lt;/p&gt;

&lt;p&gt;As a final step before running the actual tests, EC2 virtual machines are created or reused from previous jobs if they’re available. Meadowrun keeps machines around for a limited amount of time after they’re idle, to save on startup times. Meadowrun also tries to pack more than one worker on the same machine if it’s cost-effective, so the number of workers is likely greater than the number of machines. Here we’ve asked for 2 workers with 2 vCPUs and 6GiB of memory each, which will all be running on a single m5.xlarge machine. pytest-cloudist uses cheap spot instances by default.&lt;/p&gt;

&lt;p&gt;With a &lt;strong&gt;lukewarm start&lt;/strong&gt; (defined shortly) and two workers, running the tests takes about 11.5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==== 145376 passed, 21594 skipped, 1180 deselected, 1225 xfailed, 1 xpassed, 63 warnings in 685.60s (0:11:25) ===

real 11m36.304s
user 1m59.359s
sys 0m6.353s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lukewarm start means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The container with the virtual environment has been built and cached in ECR which saves about 2 minutes of container building.&lt;/li&gt;
&lt;li&gt;The code has been uploaded to S3. For pandas, the zip file is about 230MB and this takes about 1min 30sec.&lt;/li&gt;
&lt;li&gt;No EC2 instances have been created or warmed up yet. This means an EC2 instance needs to be created and booted, and the instance also needs to pull the cached Docker container image from ECR which takes about 15 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;strong&gt;cold start&lt;/strong&gt; takes about 3 minutes longer than the lukewarm start, although there can be a good amount of variation on creating and launching an EC2 instance.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;warm start&lt;/strong&gt; means that suitable EC2 instances are already running and have the necessary Docker container available locally. Running the tests in this case takes almost 9 minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=========================================================== 145376 passed, 21594 skipped, 1180 deselected, 1224 xfailed, 3 xpassed, 63 warnings in 522.77s (0:08:42) ============================================================

real 8m50.467s
user 1m10.620s
sys 0m2.983s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in the best case of a warm start, pytest-cloudist is about 1min 20 sec slower than pytest-xdist when we use two workers.&lt;/p&gt;

&lt;p&gt;We’re obsessed with &lt;a href="https://medium.com/@meadowrun/why-starting-python-on-a-fresh-ec2-instance-takes-over-a-minute-7a329e79f51"&gt;Meadowrun performance&lt;/a&gt;, and we hope to close this gap over time. The lowest hanging fruit is to &lt;a href="https://github.com/meadowdata/meadowrun/issues/158"&gt;make code upload smarter&lt;/a&gt;. That said, there’s not much we can do about how long it takes for pip to build the virtualenv, or for AWS to launch an EC2 instance.&lt;/p&gt;

&lt;h4&gt;
  
  
  Turn it to eleven
&lt;/h4&gt;

&lt;p&gt;Unlike pytest-xdist, however, we can easily add more workers now. Just by changing &lt;code&gt;--cd-num-workers&lt;/code&gt; we can speed up our tests even more:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oxgTIfuC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/752/1%2ADjJjmxrIubK1HkVMZyoSnw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oxgTIfuC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/752/1%2ADjJjmxrIubK1HkVMZyoSnw.png" alt="" width="752" height="137"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We decreased the tasks per worker target (--cd-task-per-worker-target) as we added more workers, because finer-grained tasks come with more overhead. The “Lukewarm” and “Warm” columns give the runtime under those conditions and “Lukewarm-Warm” shows the difference between the two. This difference mostly represents EC2 startup overhead.&lt;/p&gt;

&lt;p&gt;There are clearly diminishing returns as more workers are added, but it’s still cool that we can run all of these tests in about 3 minutes, especially considering that the non-parallelizable portion takes up about 2 of those 3 minutes. On top of that, having my laptop available while I was running these tests in the cloud was great — a much better experience than running them locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This post introduced pytest-cloudist, a pytest plugin that distributes tests to EC2 virtual machines using Meadowrun. As a case study, we used it to distribute a subset of the pandas tests. Developing this plugin drove a number of performance and feature enhancements in Meadowrun in the &lt;a href="https://github.com/meadowdata/meadowrun/releases"&gt;recent 0.2 releases&lt;/a&gt;, and has given us ideas for a few more.&lt;/p&gt;

&lt;p&gt;We hope you are inspired to give &lt;a href="https://github.com/kurtschelfthout/pytest-cloudist"&gt;pytest-cloudist&lt;/a&gt; and &lt;a href="https://meadowrun.io"&gt;Meadowrun&lt;/a&gt; a try. Do &lt;a href="https://gitter.im/meadowdata/meadowrun?utm_source=badge&amp;amp;utm_medium=badge&amp;amp;utm_campaign=pr-badge&amp;amp;utm_content=badge"&gt;get in touch&lt;/a&gt; for feedback, questions or just to hang out!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To stay updated, star us on&lt;/em&gt; &lt;a href="https://github.com/meadowdata/meadowrun"&gt;&lt;em&gt;Github&lt;/em&gt;&lt;/a&gt; &lt;em&gt;or follow us on&lt;/em&gt; &lt;a href="https://twitter.com/kurt2001?s=21&amp;amp;t=66yV7Xy4agKRFj4dOaLLew"&gt;&lt;em&gt;Twitter&lt;/em&gt;&lt;/a&gt;&lt;em&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pytest</category>
      <category>testing</category>
      <category>python</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Run Stable Diffusion on EC2</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Fri, 02 Sep 2022 14:36:00 +0000</pubDate>
      <link>https://dev.to/meadowrun/how-to-run-stable-diffusion-on-ec2-g86</link>
      <guid>https://dev.to/meadowrun/how-to-run-stable-diffusion-on-ec2-g86</guid>
      <description>&lt;h4&gt;
  
  
  Use Meadowrun to run the latest text-to-image model on AWS
&lt;/h4&gt;

&lt;p&gt;Stable Diffusion is a new, open text-to-image model from &lt;a href="https://stability.ai/blog/stable-diffusion-announcement"&gt;Stability.ai&lt;/a&gt; which has &lt;a href="https://simonwillison.net/2022/Aug/29/stable-diffusion/"&gt;blown&lt;/a&gt; &lt;a href="https://twitter.com/nabeelqu/status/1562986752885035009?s=20&amp;amp;t=vmXkNyh-cPt8u2nOMr0CFg"&gt;people&lt;/a&gt; &lt;a href="https://news.ycombinator.com/item?id=32555028"&gt;away&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The publicly available &lt;a href="https://beta.dreamstudio.ai/dream"&gt;official tool&lt;/a&gt; gives you 200 image generations for free, and then charges about 1¢ per image generation after that. But because the model is open, you can download the code and the model and run your own version of it. The &lt;a href="https://www.reddit.com/r/StableDiffusion/wiki/guide/"&gt;r/StableDiffusion&lt;/a&gt; subreddit has a good guide to doing this, and the options boil down to using Google Colab which requires a Colab Pro subscription ($9.99/month) to get enough GPUs, or running locally on your laptop which requires a GPU with at least 10GB of VRAM.&lt;/p&gt;

&lt;p&gt;We’ll present a different option here, which is to use Meadowrun to rent a GPU machine from AWS EC2 for just a few minutes at a time. &lt;a href="https://meadowrun.io/"&gt;Meadowrun&lt;/a&gt; is an open source library that makes it easy to run your python code on the cloud. It will take care of launching an EC2 instance, getting our code and libraries onto it, and turning it off when we’re done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Dssaz5wX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AM6NAA1kyyQK4IL6M60o1-g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Dssaz5wX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AM6NAA1kyyQK4IL6M60o1-g.png" alt="" width="880" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Images generated by Stable Diffusion from “a digital illustration of a steampunk computer floating among clouds, detailed”&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  AWS and Meadowrun Prerequisites
&lt;/h4&gt;

&lt;p&gt;First, we’ll need an AWS account where we’ve increased our quotas for GPU instances (from the default of 0). We’ll also need a local python environment with Meadowrun installed. We covered both of these steps in a previous article on running &lt;a href="https://www.craiyon.com/"&gt;Craiyon&lt;/a&gt; aka DALL·E Mini (not to be confused with OpenAI’s &lt;a href="https://openai.com/dall-e-2/"&gt;DALL·E&lt;/a&gt;) so we’ll link to the &lt;a href="https://dev.to/meadowrun/run-your-own-dalle-mini-craiyon-server-on-ec2-1pjh#prerequisites"&gt;instructions for these steps&lt;/a&gt; from that article rather than repeating them here. We recommend checking this out sooner rather than later, as it seems like AWS has a human in the loop for granting quota increases, and in our experience it can take up to a day or two to get a quota increase granted.&lt;/p&gt;
&lt;h4&gt;
  
  
  Stable Diffusion Prerequisites
&lt;/h4&gt;

&lt;p&gt;Next, we’ll need to go to the &lt;a href="https://huggingface.co/CompVis/stable-diffusion-v-1-4-original"&gt;Stable Diffusion page&lt;/a&gt; on Hugging Face, accept the terms, and download the &lt;a href="https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/blob/main/sd-v1-4.ckpt"&gt;checkpoint file&lt;/a&gt; containing the model weights to our local machine.&lt;/p&gt;

&lt;p&gt;Then, we’ll create an S3 bucket and upload this file to our new bucket so that our EC2 instance can access this file. From the directory where the checkpoint file was downloaded, we’ll run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 mb s3://meadowrun-sd
aws s3 cp sd-v1-4.ckpt s3://meadowrun-sd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that S3 bucket names are globally unique, so you’ll need to use a unique bucket name that’s different from what we’re using here (&lt;code&gt;meadowrun-sd&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Finally, we’ll need to grant access to this bucket for the Meadowrun-launched EC2 instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meadowrun-manage-ec2 grant-permission-to-s3-bucket meadowrun-sd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Running Stable Diffusion
&lt;/h4&gt;

&lt;p&gt;Now we’re ready to run Stable Diffusion!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;meadowrun&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;folder_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"steampunk_computer"&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"a digital illustration of a steampunk computer floating among clouds, detailed"&lt;/span&gt;

    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;'bash -c &lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
            &lt;span class="s"&gt;'aws s3 sync s3://meadowrun-sd /var/meadowrun/machine_cache --exclude "*" '&lt;/span&gt;
            &lt;span class="s"&gt;'--include sd-v1-4.ckpt '&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&amp;amp;&amp;amp; python scripts/txt2img.py --prompt "&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;" --plms '&lt;/span&gt;
            &lt;span class="s"&gt;'--ckpt /var/meadowrun/machine_cache/sd-v1-4.ckpt --outdir /tmp/outputs '&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;'&amp;amp;&amp;amp; aws s3 sync /tmp/outputs s3://meadowrun-sd/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;folder_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EC2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;gpu_memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"nvidia"&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"https://github.com/hrichardlee/stable-diffusion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"meadowrun-compatibility"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CondaEnvironmentYmlFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"environment.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;additional_software&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"awscli"&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;environment_variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s"&gt;"TRANSFORMERS_CACHE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/var/meadowrun/machine_cache/transformers"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s walk through this snippet. The first parameter to &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.run_command"&gt;run_command&lt;/a&gt; tells Meadowrun what we want to run on the remote machine. In this case we’re using bash to chain three commands together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, we’ll use &lt;code&gt;aws s3 sync&lt;/code&gt; to download the weights from S3. Our command will run in a container, but the &lt;code&gt;/var/meadowrun/machine_cache&lt;/code&gt; folder that we download into &lt;a href="https://docs.meadowrun.io/en/stable/how_to/machine_cache/"&gt;can be used to cache data&lt;/a&gt; for multiple jobs that run on the same instance. &lt;code&gt;aws s3 cp&lt;/code&gt; &lt;a href="https://github.com/aws/aws-cli/issues/2874"&gt;doesn’t have&lt;/a&gt; a &lt;code&gt;--no-overwrite&lt;/code&gt; option, so we use &lt;code&gt;aws s3 sync&lt;/code&gt; to only download the file if we don’t already have it. This isn’t robust to multiple processes running concurrently on the same machine, but in this case we’re only running one command at a time.&lt;/li&gt;
&lt;li&gt;Second, we’ll run the &lt;a href="https://github.com/hrichardlee/stable-diffusion/blob/main/scripts/txt2img.py"&gt;txt2img.py&lt;/a&gt; script which will generate images from the prompt we specify.&lt;/li&gt;
&lt;li&gt;The last part of our command will then upload the outputs of the txt2img.py script into our same S3 bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next two parameters tell Meadowrun what kind of instance we need to run our code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AllocCloudInstance("EC2")&lt;/code&gt; tells Meadowrun to provision an EC2 instance.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.Resources"&gt;Resources&lt;/a&gt; tells Meadowrun the requirements for the EC2 instance. In this case we’re requiring at least 1 CPU, 8 GB of main memory, and 10GB of GPU memory on an Nvidia GPU. We also set &lt;code&gt;max_eviction_rate&lt;/code&gt; to 80 which means we’re okay with spot instances up to an 80% chance of interruption. The GPU instances we’re using are fairly popular, so if our instance is interrupted or evicted frequently, we might need to switch to an on-demand instance by setting this parameter to 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.deployment_spec.Deployment.git_repo"&gt;Deployment.git_repo&lt;/a&gt; specifies our python dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first two parameters tell Meadowrun to get the code from the meadowrun-compatibility branch of &lt;a href="https://github.com/hrichardlee/stable-diffusion"&gt;this fork&lt;/a&gt; of the &lt;a href="https://github.com/CompVis/stable-diffusion"&gt;official repo&lt;/a&gt;. We were almost able to use the original repo as-is, but we had to make a &lt;a href="https://github.com/hrichardlee/stable-diffusion/commit/b0cc2e6fa0cd17275bbdee41b974aac0f94b39d3"&gt;small tweak&lt;/a&gt; to the environment.yaml file — Meadowrun doesn’t yet support installing the current code as an editable pip package.&lt;/li&gt;
&lt;li&gt;The third parameter tells Meadowrun to create a conda environment based on the packages specified in the &lt;a href="https://github.com/hrichardlee/stable-diffusion/blob/meadowrun-compatibility/environment.yaml"&gt;environment.yaml&lt;/a&gt; file in the repo.&lt;/li&gt;
&lt;li&gt;We also need to tell Meadowrun to install awscli, which is a non-conda dependency installed via &lt;code&gt;apt&lt;/code&gt;. We’re using the AWS CLI to download and upload files to/from S3.&lt;/li&gt;
&lt;li&gt;The last parameter sets the &lt;code&gt;TRANSFORMERS_CACHE&lt;/code&gt; environment variable. Stable Diffusion uses Hugging Face’s &lt;a href="https://github.com/huggingface/transformers"&gt;transformers&lt;/a&gt; library which downloads model weights. This environment variable points transformers to the &lt;code&gt;/var/meadowrun/machine_cache&lt;/code&gt; folder so that we can reuse this cache across runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To walk through selected parts of the output, first Meadowrun tells us everything we need to know about the instance it started for this job and how much it will cost us (only 16¢ per hour for the spot instance! If we need the on-demand instance it will cost 53¢ per hour).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launched a new instance for the job: ec2-3-15-146-110.us-east-2.compute.amazonaws.com: g4dn.xlarge (4.0 CPU, 16.0 GB, 1.0 GPU), spot ($0.1578/hr, 61.0% eviction rate), will run 1 workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, Meadowrun builds a container based on the contents of the environment.yaml file we specified. This takes a while, but Meadowrun caches the image in &lt;a href="https://aws.amazon.com/ecr/"&gt;ECR&lt;/a&gt; for us so this only needs to happen once. Meadowrun also cleans up the image if we don’t use it for a while.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Building python environment in container a07bf5...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, we’ll see the output from the txt2img.py script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Global seed set to 42
Loading model from /var/meadowrun/machine_cache/sd-v1-4.ckpt
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script usually around 3 minutes and generates 6 images with the default settings. This adds up to about 6 images for 1¢ with a spot instance, and about 2 images for 1¢ with an on-demand instance, although we do have to pay for some overhead for creating the environment.&lt;/p&gt;

&lt;p&gt;Once the last command completes, our images will be available in our S3 bucket! We can view them using an S3 UI like &lt;a href="https://cyberduck.io/"&gt;CyberDuck&lt;/a&gt; or just sync the bucket to our local machine using the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync s3://meadowrun-sd/steampunk_computer steampunk_computer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meadowrun will automatically turn off the machine if we don’t use it for 5 minutes, but if we know we’re done generating images, we can turn it off manually from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meadowrun-manage-ec2 clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Closing remarks
&lt;/h4&gt;

&lt;p&gt;Stable Diffusion is remarkable for how good it is, how open it is, and how cheap and easy it is to use. And &lt;a href="https://meadowrun.io"&gt;Meadowrun&lt;/a&gt; makes it even easier!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To stay updated on Meadowrun, star us on &lt;a href="https://github.com/meadowdata/meadowrun"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001"&gt;Twitter&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stablediffusion</category>
      <category>gpu</category>
      <category>ec2</category>
      <category>python</category>
    </item>
    <item>
      <title>Kubernetes Was Never Designed for Batch Jobs</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Thu, 01 Sep 2022 13:58:29 +0000</pubDate>
      <link>https://dev.to/meadowrun/kubernetes-was-never-designed-for-batch-jobs-92k</link>
      <guid>https://dev.to/meadowrun/kubernetes-was-never-designed-for-batch-jobs-92k</guid>
      <description>&lt;p&gt;In this post we’ll make the case that Kubernetes is philosophically biased towards microservices over batch jobs. This results in an impedance mismatch that makes it harder than it “should” be to use Kubernetes for batch jobs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Batch world vs microservice world
&lt;/h4&gt;

&lt;p&gt;One thing that took me a while to figure out was why so many wildly popular technologies felt so weird to me. It clicked when I realized that I was living in “batch world”, and these technologies were coming out of “microservice world”.&lt;/p&gt;

&lt;p&gt;I’ve mostly worked at hedge funds, and the ones I’ve worked at are part of batch world. This means that code is usually run when triggered by an external event, like a vendor sending the closing prices for US equities for that day or a clearinghouse sending the list of trades that it recorded for the day. A job scheduler like Airflow triggers a job to run depending on which event happened. That job usually triggers other jobs, and the ultimate output of these jobs is something like an API call to an external service that executes trades or a PnL (profit and loss) report that gets emailed to humans.&lt;/p&gt;

&lt;p&gt;In contrast, in microservice world (and take this with a grain of salt, as I’ve only ever touristed there), instead of jobs that run to completion, everything is a long-running service. So instead of a job that ingests closing US equity prices and then kicks off a job that calculates the PnL report, there might be a PnL report service that polls the prices service until the prices it needs are available.&lt;/p&gt;

&lt;h4&gt;
  
  
  But what’s the difference, really?
&lt;/h4&gt;

&lt;p&gt;The difference between batch jobs and services seems easy to define — batch jobs are triggered by some “external” event, do some computation and then exit on their own. Services on the other hand run forever in a loop that accepts requests, does a smaller bit of computation, and then responds to those requests.&lt;/p&gt;

&lt;p&gt;But we could describe batch jobs in a way that makes them sound like services by saying that each invocation of a batch job is a “request”. In that view, the overall pattern of running batch jobs looks like a service with the job scheduler playing the role of the load balancer and each batch job invocation plays the role of handling a request. This pattern of handling each request in its own process is similar to the fairly common “&lt;a href="https://unixism.net/2019/04/linux-applications-performance-part-ii-forking-servers/"&gt;forking server&lt;/a&gt;” pattern.&lt;/p&gt;

&lt;p&gt;And vice versa, consider the thought experiment where we’ve broken up our code into thousands of different microservices but we don’t have enough hardware to run all of these microservices at the same time. So we configure a sophisticated, responsive autoscaler that only starts each microservice for the duration of a single request/reply when a request comes in. At that point, our autoscaler is starting to look more like a job scheduler, and our microservices kind of look like batch jobs!&lt;/p&gt;

&lt;p&gt;Let’s clarify our original intuition — our description of services “running forever” is imprecise. It’s true that most services don’t exit “successfully” on their own, but they might exit due to a catastrophic exception, in response to an autoscaling decision, or as part of a deployment process. So what are we actually getting at when we say that services “run forever”? The most important aspect is that we think of services as “stateless” which can also be stated as “services are easily restartable”. Because each request/reply cycle is usually on the order of milliseconds to seconds and there’s usually minimal state in the server process outside of the request handler, restarting a service shouldn’t lose us more than a few seconds of computation. In contrast, batch jobs are generally “not restartable”, which is to say that if we have a batch job that takes several hours to run and we restart it after one hour, we will need to redo that first hour of work which is usually unacceptable. If we somehow wrote a batch job that created checkpoints of its state every 5 seconds, thereby making it more or less “stateless”, then that batch job would be just as “easily restartable” as a service.&lt;/p&gt;

&lt;p&gt;So the most defensible distinction between service and batch jobs is that services do a small (milliseconds to seconds) amount of work at a time which makes them easy to restart, while batch jobs do a large (minutes to hours) amount of work at a time which makes them harder to restart. Stated this way, the difference between services and batch jobs is fuzzier than it first appears.&lt;/p&gt;

&lt;p&gt;But just because we can come up with hard-to-categorize examples that challenge this binary, it doesn’t mean the difference is meaningless. In practice, services generally run as long-lived processes that respond to requests, do a small bit of work for each request and then respond. Meanwhile batch jobs are usually triggered ad-hoc by a data scientist or from a job scheduler and do a large amount of work at a time which makes them harder to restart.&lt;/p&gt;

&lt;p&gt;Another way to see the difference between services vs batch jobs is how they scale. Scaling a service usually means dealing with a large number of concurrent requests by having an autoscaler run replicas of the service with a load balancer sending different requests to different replicas. Scaling a batch job usually usually means dealing with a large amount of data by running the same code over different chunks of data in parallel. We’ll go into these aspects below in more depth as we walk through how well (or poorly) Kubernetes supports these scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Kubernetes sees the world
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Technology is neither good nor bad; nor is it neutral. &lt;a href="https://en.wikipedia.org/wiki/Melvin_Kranzberg"&gt;Melvin Kranzberg&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll skip the full history and functionality of Kubernetes as there’s plenty of existing coverage (&lt;a href="https://blog.coinbase.com/container-technologies-at-coinbase-d4ae118dcb6c"&gt;this post from Coinbase&lt;/a&gt; gives a good overview). Our focus here is understanding Kubernetes’ philosophy — how does it see the world and what kinds of patterns does it create for users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mTXo_zv_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2ABYjUayTLYZUXVEgddclwGA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mTXo_zv_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2ABYjUayTLYZUXVEgddclwGA.jpeg" alt="" width="880" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;It’s all about philosophy. The Thinker in The Gates of Hell at the Musée Rodin via Wikimedia Commons&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Kubernetes is for services
&lt;/h4&gt;

&lt;p&gt;We’ll start with the “&lt;a href="https://kubernetes.io/docs/concepts/overview/#why-you-need-kubernetes-and-what-can-it-do"&gt;what Kubernetes can do&lt;/a&gt;” section on the “Overview” page in the Kubernetes documentation. Most of the key features listed here are focused on services rather than batch jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Service discovery and load balancing” resolves domain names to one or more replicas of a service. This isn’t relevant for batch jobs which usually don’t have a concept of request/response, so there’s no need to resolve domain names to containers, or round-robin requests between different instances of a service.&lt;/li&gt;
&lt;li&gt;“Automated rollouts and rollbacks” makes deployment of services easier by turning off a few instances, restarting them with the new deployment, and then repeating until all of the instances have been updated. This idea doesn’t apply to batch jobs because batch jobs are “hard to restart” and naturally exit on their own, so the right thing to do is to wait until the batch job finishes and then start subsequent jobs with the new deployment rather than losing work to a restart. And we certainly wouldn’t want a rolling deployment to result in a distributed job where different tasks run on different version of our code!&lt;/li&gt;
&lt;li&gt;“Self-healing”: Restarting jobs that fail is useful, but batch jobs don’t have a concept of a “health check”, and not advertising services to clients until they’re ready isn’t relevant to batch jobs.&lt;/li&gt;
&lt;li&gt;“Automatic bin packing” is only partially relevant for batch jobs — we definitely want to be smart about the initial allocation of where we run a job, but again, batch jobs can’t be restarted willy-nilly, so they can’t be “moved” to a different node.&lt;/li&gt;
&lt;li&gt;“Secret and configuration management” and “Storage orchestration” are equally relevant for services and batch jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One theme throughout these features is that Kubernetes assumes that the code it’s running is relatively easy to restart. In other words, it assumes it’s running services.&lt;/p&gt;

&lt;h4&gt;
  
  
  Kubernetes doesn’t believe in orchestration
&lt;/h4&gt;

&lt;p&gt;That same &lt;a href="https://kubernetes.io/docs/concepts/overview/#what-kubernetes-is-not"&gt;Overview page&lt;/a&gt; declares:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes is not a mere orchestration system. In fact, it eliminates the need for orchestration. The technical definition of orchestration is execution of a defined workflow: first do A, then B, then C. In contrast, Kubernetes comprises a set of independent, composable control processes that continuously drive the current state towards the provided desired state. It shouldn’t matter how you get from A to C. Centralized control is also not required. This results in a system that is easier to use and more powerful, robust, resilient, and extensible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This paragraph presumably refers to the idea that in Kubernetes you define your configuration declaratively (e.g. make sure there are 3 instances of this service running at all times) rather than imperatively (e.g. check how many instances are running, if there more than 3, kill instances until there are 3 remaining; if there are fewer than 3, start instances until we have 3).&lt;/p&gt;

&lt;p&gt;Nevertheless, job schedulers like Airflow are orchestration frameworks in the exact way this paragraph describes. And of course we can just run Airflow on top of Kubernetes to work around this bit of philosophy, but Kubernetes intentionally makes it hard to implement this kind of orchestration natively.&lt;/p&gt;

&lt;p&gt;Kubernetes has a “&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/"&gt;job&lt;/a&gt;” concept for running batch jobs, but the aversion to the idea of “first do A, then B” means that the job API will probably never be able to express this core concept. The only hope for expressing job dependencies in Kubernetes would be a declarative model — “in order to run B, A must have run successfully”. But this feature doesn’t exist either, and while a fully declarative model does have its advantages, it’s not currently the dominant paradigm for expressing job dependencies.&lt;/p&gt;

&lt;p&gt;Moreover, jobs are clearly secondary to services in Kubernetes. Jobs don’t appear in that Overview page at all, and are mostly ignored throughout the documentation aside from the sections that are explicitly about jobs. One conspicuous absence is &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions/"&gt;this page&lt;/a&gt; which states “Pods do not disappear until someone (a person or a controller) destroys them, or there is an unavoidable hardware or system software error.” This inexplicably ignores the case where pods for jobs complete naturally.&lt;/p&gt;

&lt;h3&gt;
  
  
  More missing features
&lt;/h3&gt;

&lt;p&gt;Not only are Kubernetes jobs themselves not fully featured, they exist in a larger system that is designed with a philosophy that makes jobs less useful than they could be. For the rest of this article, we’ll examine some of these details of Kubernetes and why they make life harder for batch jobs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pod preemption
&lt;/h4&gt;

&lt;p&gt;Both jobs and services are implemented as “&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/"&gt;pods&lt;/a&gt;” which are neutral in theory but in practice biased towards services. One example is that pods can be preempted to make room for other pods, which assumes that pods are easily restarted. The documentation acknowledges this &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#non-preempting-priority-class"&gt;isn’t appropriate for batch jobs&lt;/a&gt; but the recommendation it makes is a bit backwards — it suggests that batch jobs should set preemptionPolicy: Never. This means those pods will never preempt another job which only works if all of the pods on the cluster do the same thing. Ideally there would be a way to guarantee that the pod itself would never be preempted even in a cluster that runs both batch jobs and services. There are workarounds like reserving higher priorities for batch jobs or using &lt;a href="https://kubernetes.io/docs/tasks/run-application/configure-pdb/"&gt;pod disruption budgets&lt;/a&gt;, but these aren’t mentioned on that page. This is exactly what we mean by an impedance mismatch — we can ultimately accomplish what we need to, but it takes more effort than it “should”.&lt;/p&gt;

&lt;h4&gt;
  
  
  Composability
&lt;/h4&gt;

&lt;p&gt;Kubernetes only works with containers, and containers themselves are also biased towards services in how they compose. If we have two containers that need to talk to each other, exposing services in one or both of the containers is the only game in town.&lt;/p&gt;

&lt;p&gt;For example, let’s say we want to run some Python code and that code needs to call &lt;a href="https://imagemagick.org/index.php"&gt;ImageMagick&lt;/a&gt; to do some image processing. We want to run the Python code in a container based on the &lt;a href="https://hub.docker.com/_/python"&gt;Python Docker image&lt;/a&gt; and we want to run ImageMagick in a separate container based on &lt;a href="https://hub.docker.com/r/dpokidov/imagemagick"&gt;this ImageMagick Docker image&lt;/a&gt;. Let’s think about our options for calling the ImageMagick container from the Python container, i.e. for composing these two containers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We could use the Python image as a base image and copy parts of the ImageMagick Dockerfile into a new Dockerfile that builds a custom combined image. This is probably the most practical solution, but it has all the usual drawbacks of copy/paste — any improvements to the ImageMagick Dockerfile won’t make it into our Dockerfile without manually updating our Dockerfile.&lt;/li&gt;
&lt;li&gt;We could invoke the ImageMagick container as if it were a command line application. Kubernetes supports &lt;a href="https://kubernetes.io/docs/tasks/access-application-cluster/communicate-containers-same-pod-shared-volume/"&gt;sharing files between containers in the same pod&lt;/a&gt; so at least we can send our inputs/outputs back and forth, but there isn’t a great way to invoke a command and get notified when it’s done. Anything is possible, of course (e.g. starting a new job and polling the pods API to see when it completes), but Kubernetes’ philosophical aversion to orchestration is not helping here.&lt;/li&gt;
&lt;li&gt;We could modify the ImageMagick image to expose a service. This seems silly, but is effectively what happens most of the time — instead of building command-line tools like ImageMagick, people end up building services to sidestep this problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In fact in Docker, “&lt;a href="https://docs.docker.com/compose/"&gt;Compose&lt;/a&gt;” means combining services:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even if we acquiesce to the idea of composing via services, Kubernetes doesn’t give us great options for running a service in the context where a batch job is the consumer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The simplest option is to run one or more copies of the ImageMagick service constantly, but that’s a waste of resources when we’re not running anything that needs the service, and it will be overwhelmed when we launch a distributed job running thousands of concurrent tasks.&lt;/li&gt;
&lt;li&gt;So we might reach for the &lt;a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/"&gt;HorizontalPodAutoscaler&lt;/a&gt; to spin up instances when we need them and turn them off when we don’t. This works, but to get the responsiveness of calling something on the command line, we’ll need to make the autoscaler’s “sync-period” much shorter than the default 15 seconds.&lt;/li&gt;
&lt;li&gt;Another option is “&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods"&gt;sidecar containers&lt;/a&gt;” where we run the Python image and the ImageMagick service image in the same pod. This mostly works, but &lt;a href="https://stackoverflow.com/questions/38600622/sidecar-containers-in-kubernetes-pods"&gt;there’s no way to automatically kill&lt;/a&gt;the service’s sidecar container when the “main” batch job container is done. The proposed feature to allow this was &lt;a href="https://github.com/kubernetes/enhancements/issues/753#issuecomment-713471597"&gt;ultimately rejected&lt;/a&gt; because it “is not an incremental step in the right direction”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exploring these options for how batch jobs can call other containers shows that we can make it work, but Kubernetes makes it harder than it “should” be.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ad-hoc jobs
&lt;/h4&gt;

&lt;p&gt;One aspect of batch jobs is that we often run them ad-hoc for research, development, or troubleshooting. For example, we edit some code or tweak some data, and then rerun our linear regression to see if we get better results. For these ad-hoc jobs there would ideally be some way to take e.g. a CSV file that we’re working with locally, and “upload” it to a volume that we could read from a pod. This scenario isn’t supported by Kubernetes, though, so we have to figure out other ways to get data into our pod.&lt;/p&gt;

&lt;p&gt;One option would be to set up an NFS (Network File System) that’s accessible from outside of the cluster and &lt;a href="https://kubernetes.io/docs/concepts/storage/volumes/#nfs"&gt;expose it&lt;/a&gt; to our pods in the cluster. The other option is, as usual, a service of some sort. We could use a queue like &lt;a href="https://www.rabbitmq.com/"&gt;RabbitMQ&lt;/a&gt; that will temporarily store our data and make it available to our pod.&lt;/p&gt;

&lt;p&gt;But with a service, we now have another problem of accessing this service from outside of the cluster. One way to solve this problem is to make our dev machine part of the Kubernetes cluster. Then we can configure a simple &lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/"&gt;Service&lt;/a&gt; for RabbitMQ which will be accessible from inside the cluster. If that’s not an option, though, we’ll need to explore &lt;a href="https://kubernetes.io/docs/tasks/access-application-cluster/"&gt;Accessing Applications in a Cluster&lt;/a&gt;. The options boil down to using &lt;a href="https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/"&gt;port forwarding&lt;/a&gt;, which is a debugging tool not recommended for “real” use cases, or setting up an &lt;a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/"&gt;Ingress Controller&lt;/a&gt; which is a formalized way of exposing services running in a Kubernetes cluster that is overkill for a single client accessing a single instance of a service from an internal network. None of these options are ideal.&lt;/p&gt;

&lt;p&gt;Kubernetes doesn’t make this easy because when we’re running services, we either don’t want anything outside of the cluster interacting with our service, or we want a relatively heavy-duty load balancer in front of our service to make sure our service doesn’t go down under load from the outside world. The missing feature here is some way for the client that is launching a job/pod to upload a file that the pod can access. This scenario is relatively common when running batch jobs and uncommon when running services, so it doesn’t exist in Kubernetes and we have to use these workarounds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Distributed jobs that kind of work
&lt;/h4&gt;

&lt;p&gt;Another aspect of batch jobs is that we’ll often want to run distributed computations where we split our data into chunks and run a function on each chunk. One &lt;a href="https://news.ycombinator.com/item?id=27915761"&gt;popular option&lt;/a&gt; is to run Spark, which is built for exactly this use case, on top of Kubernetes. And there &lt;a href="https://volcano.sh/en/"&gt;are&lt;/a&gt; &lt;a href="https://github.com/G-Research/armada/"&gt;other&lt;/a&gt; &lt;a href="https://github.com/kubernetes-sigs/kube-batch"&gt;options&lt;/a&gt; for additional software to make running distributed computations on Kubernetes easier.&lt;/p&gt;

&lt;p&gt;The Kubernetes documentation, however, doesn’t cede to third-party frameworks, and instead gives several &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/#job-patterns"&gt;options&lt;/a&gt; for running distributed computations directly on Kubernetes. But none of these options are compelling especially in contrast to how well-designed Kubernetes is for services workloads.&lt;/p&gt;

&lt;p&gt;The simplest option is to just &lt;a href="https://kubernetes.io/docs/tasks/job/parallel-processing-expansion/"&gt;create a single Job object per task&lt;/a&gt;. As the documentation points out, this won’t work well with a large number of tasks. &lt;a href="https://github.com/kubernetes/kubernetes/issues/95492"&gt;One user’s experience&lt;/a&gt; is that it’s hard to go beyond a few thousand jobs total. It seems like &lt;a href="https://kubernetes.io/docs/tasks/job/indexed-parallel-processing-static/"&gt;the best way around that&lt;/a&gt; is to use Indexed Jobs, which is a relatively new feature for running multiple copies of the same job where the different copies of the job have different values for the &lt;code&gt;JOB_COMPLETION_INDEX&lt;/code&gt; environment variable. This gives us the most fundamental layer for running distributed jobs. As long as each task just needs an index number and doesn’t need to “send back” any outputs, this works. E.g. if all of the tasks are working on a single file and the tasks “know” that they need to process &lt;code&gt;n&lt;/code&gt; rows that come after skipping the first &lt;code&gt;JOB_COMPLETION_INDEX * n&lt;/code&gt; rows, and then write their output to a database, this works great.&lt;/p&gt;

&lt;p&gt;But in some cases, we’ll want to run tasks that e.g. need a filename to know where their input data is, and it could be convenient to send back results directly to the process that launched the distributed job for post-processing. In other words we might need to send more data back and forth from the tasks beyond a single number. For that, the documentation offers &lt;a href="https://kubernetes.io/docs/tasks/job/coarse-parallel-processing-work-queue/"&gt;two&lt;/a&gt; &lt;a href="https://kubernetes.io/docs/tasks/job/fine-parallel-processing-work-queue/"&gt;variations&lt;/a&gt; of using a message queue service that you start in your Kubernetes cluster. The main difficulty with this approach is that we have the same problem as before of accessing services inside of the Kubernetes cluster from outside of it so that we can add messages to the message queue service. The documentation suggests &lt;a href="https://kubernetes.io/docs/tasks/job/coarse-parallel-processing-work-queue/#testing-the-message-queue-service"&gt;creating a temporary interactive Pod&lt;/a&gt; but that only really makes sense for testing. We have the same options as before — make sure everything including our dev machines run inside the cluster, use port forwarding, or create an Ingress.&lt;/p&gt;

&lt;h4&gt;
  
  
  Distributed groupby
&lt;/h4&gt;

&lt;p&gt;An additional wrinkle with distributed computations is distributed “groupbys”. Distributed groupbys are necessary when our dataset is “chunked” or “grouped” by one column (e.g. date) but we want to group by a different column (e.g. zip code) before applying a computation. This requires re-grouping our original chunks, which is implemented as a “&lt;a href="https://spark.apache.org/docs/2.3.0/rdd-programming-guide.html#shuffle-operations"&gt;shuffle&lt;/a&gt;” (also known as a map-reduce). Worker-to-worker communication is central to shuffles. Each per-date worker gets a chunk of data for a particular date, and then sends rows for each zip code to a set of per-zip code downstream workers. The per-zip code workers receive the rows for “their” zip code from each upstream per-date worker.&lt;/p&gt;

&lt;p&gt;To implement this worker-to-worker communication, Kubernetes could implement some version of &lt;a href="https://kubernetes.io/docs/tasks/access-application-cluster/communicate-containers-same-pod-shared-volume/"&gt;shared volumes&lt;/a&gt; that would allow us to expose the results from one pod to a downstream pod that needs those outputs. Again, this functionality would be really useful in “batch world”, but the use case doesn’t exist in “service world”, so this functionality doesn’t exist. Instead we need to write our own service for worker-to-worker communication.&lt;/p&gt;

&lt;p&gt;This is what, for example, Spark does, but it runs into an impedance mismatch as well. Spark’s shuffle implementation makes extensive use of local disk in order to deal with data that doesn’t fit in memory. But Kubernetes makes it hard to get the full performance of your local disks because it only allows for disk caching at the container, meaning that the kernel’s disk caching isn’t used and Spark’s shuffle performance &lt;a href="https://news.ycombinator.com/item?id=23153981"&gt;suffers&lt;/a&gt; on Kubernetes. A more native way to share files across pods would enable a faster implementation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Caching data
&lt;/h4&gt;

&lt;p&gt;Another aspect of distributed computations that we’ll talk about is caching data. Most distributed computations have the concept of a distributed dataset that’s cached on the workers that can be reused later. For example, Spark has the concept of &lt;a href="https://spark.apache.org/docs/2.3.0/rdd-programming-guide.html"&gt;RDDs&lt;/a&gt; (resilient distributed dataset). We can cache an RDD in Spark, which means that each Spark worker will store one or more chunks of the RDD. For any subsequent computations on that RDD, the worker that has a particular chunk will run the computation for that chunk. This general idea of “sending code to the data” is a crucial aspect of implementing an efficient distributed computation system.&lt;/p&gt;

&lt;p&gt;Kubernetes in general is a bit unfriendly to the idea of storing data on the node, &lt;a href="https://kubernetes.io/docs/concepts/storage/volumes/#hostpath"&gt;recommending&lt;/a&gt; that “it is a best practice to avoid the use of HostPaths when possible”. And even though there are extensive capabilities for assigning pods to nodes via &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity"&gt;affinity, anti-affinity&lt;/a&gt;, &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/"&gt;taints, tolerations&lt;/a&gt;, and &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/"&gt;topology spread constraints&lt;/a&gt;, none of these work with the concept of “what cached data is available on a particular node”.&lt;/p&gt;

&lt;p&gt;Overall, Kubernetes has some very half-hearted support for natively running distributed computations. Anything beyond the most simple scenario requires either implementing workarounds or layering on another platform like Spark.&lt;/p&gt;

&lt;h3&gt;
  
  
  The future of batch jobs on Kubernetes
&lt;/h3&gt;

&lt;p&gt;The point of this article is not to suggest that Kubernetes is poorly thought out or poorly implemented. Arguably, &lt;a href="https://news.ycombinator.com/item?id=23359443"&gt;if you have a monolithic service&lt;/a&gt;, Kubernetes is overkill, and there’s at least one person predicting it will be &lt;a href="https://news.ycombinator.com/item?id=22402315"&gt;gone in 5 years&lt;/a&gt;, but it seems like a great choice for e.g. &lt;a href="https://news.ycombinator.com/item?id=26272144"&gt;this person running 120 microservices&lt;/a&gt;. And it makes deploying your web app with a &lt;a href="https://news.ycombinator.com/item?id=27910150"&gt;database, DNS records, and SSL certificates&lt;/a&gt; easy. What we’re saying here is that Kubernetes, like all technologies, takes a point of view, and that point of view isn’t particularly friendly to batch jobs.&lt;/p&gt;

&lt;p&gt;Instead of the currently half-hearted support for batch jobs, one option we’d love to see is Kubernetes making its stance more explicit and declaring that Kubernetes is designed primarily for services. This could open up space for other platforms more specifically designed for the batch job use case. Alternatively, as Kubernetes is already a “&lt;a href="https://twitter.com/kelseyhightower/status/935252923721793536"&gt;platform for building platforms&lt;/a&gt;” in some ways, we could see something like Spark-on-Kubernetes become better supported. It seems unlikely that Kubernetes would adopt a significantly different overall philosophy where batch jobs are a first-class use case.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Star us on &lt;a href="https://github.com/meadowdata/meadowrun"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001"&gt;Twitter&lt;/a&gt;! We’re working on&lt;/em&gt; &lt;a href="https://meadowrun.io/"&gt;&lt;em&gt;Meadowrun&lt;/em&gt;&lt;/a&gt; &lt;em&gt;which is an open source library that solves some of these problems and makes it easier to run your Python code on the cloud,&lt;/em&gt; &lt;a href="https://docs.meadowrun.io/en/stable/how_to/kubernetes/"&gt;&lt;em&gt;with&lt;/em&gt;&lt;/a&gt; &lt;em&gt;or without Kubernetes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>mlops</category>
      <category>dataengineering</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to Deploy ML Models Using Gravity AI and Meadowrun</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Wed, 17 Aug 2022 15:58:00 +0000</pubDate>
      <link>https://dev.to/meadowrun/how-to-deploy-ml-models-using-gravity-ai-and-meadowrun-1mjn</link>
      <guid>https://dev.to/meadowrun/how-to-deploy-ml-models-using-gravity-ai-and-meadowrun-1mjn</guid>
      <description>&lt;h4&gt;
  
  
  Transform your containerized models-as-services into batch jobs
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://www.gravity-ai.com/" rel="noopener noreferrer"&gt;GravityAI&lt;/a&gt; is a marketplace for ML models where data scientists can &lt;a href="https://www.gravity-ai.com/pages/Partners" rel="noopener noreferrer"&gt;publish&lt;/a&gt; their models as &lt;a href="https://www.gravity-ai.com/blogs/Automatically_Conatainerize_a_Model" rel="noopener noreferrer"&gt;containers&lt;/a&gt;, and consumers can subscribe to access those models. In this article, we’ll talk about different options for deploying these containers and then walk through using them for batch jobs via &lt;a href="https://meadowrun.io/" rel="noopener noreferrer"&gt;Meadowrun&lt;/a&gt;, which is an open-source library for running your Python code and containers in the cloud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F0%2AzyiOfS3rBDj2MEkG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F0%2AzyiOfS3rBDj2MEkG" alt="Runner on a starting block"&gt;&lt;/a&gt;&lt;br&gt;&lt;em&gt;A library inside of a container. Photo by Manuel Palmeira on Unsplash&lt;/em&gt;
  &lt;/p&gt;

&lt;h4&gt;
  
  
  Containers vs libraries
&lt;/h4&gt;

&lt;p&gt;This section lays out some motivation for this post — if you’re interested in just getting things working, please go to the next section!&lt;/p&gt;

&lt;p&gt;If you think of an ML model as a library, then it might seem more natural to publish it as a package, either on &lt;a href="https://pypi.org/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; for use with pip or &lt;a href="https://anaconda.org/meadowdata/dashboard" rel="noopener noreferrer"&gt;Anaconda.org&lt;/a&gt; for use with conda, rather than a container. Hugging Face’s &lt;a href="https://huggingface.co/docs/transformers/quicktour" rel="noopener noreferrer"&gt;transformers&lt;/a&gt; is a good example — you run &lt;code&gt;pip install transformers&lt;/code&gt;, then your Python interpreter can do things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment-analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;We are very happy to show you the 🤗 Transformers library.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few disadvantages with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  For pip specifically (not conda), there’s no way for packages to express non-Python dependencies. With the transformers library, you’ll usually depend on something like CUDA for GPU support, but users need to know to install that separately.&lt;/li&gt;
&lt;li&gt;  The transformers library also needs to download gigabytes of model weights in order to do anything useful. These get &lt;a href="https://huggingface.co/docs/datasets/cache" rel="noopener noreferrer"&gt;cached in a local directory&lt;/a&gt; so they don’t need to be downloaded every time you run, but for some contexts you might want to make these model weights a fixed part of the deployment.&lt;/li&gt;
&lt;li&gt;  Finally, when publishing a pip or conda package, users expect you to specify your dependencies relatively flexibly. E.g. the transformers package specifies “tensorflow&amp;gt;=2.3” which declares that the package works with any version of tensorflow 2.3 or higher. This means that if the tensorflow maintainers introduce a bug or break backwards compatibility, they can effectively cause your package to stop working (welcome to &lt;a href="https://en.wikipedia.org/wiki/Dependency_hell" rel="noopener noreferrer"&gt;dependency hell&lt;/a&gt;). Flexible dependencies are useful for libraries, because it means more people can install your package into their environments. But for deployments, e.g. if you’re deploying a model within your company, you’re not taking advantage of that flexibility and you’d rather have the certainty it’s going to work every time, and reproducibility of the deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One common way to solve these problems is to build a container. You can install CUDA into your container, if you include your model weights in the image Docker will make sure you only have a single copy of that data on each machine as long as you’re managing your Docker &lt;a href="https://stackoverflow.com/questions/31222377/what-are-docker-image-layers" rel="noopener noreferrer"&gt;layers&lt;/a&gt; correctly, and you’ll be able to choose and test the specific version of tensorflow that goes into the container.&lt;/p&gt;

&lt;p&gt;So we’ve solved a bunch of problems, but we’ve traded them for a new problem, which is that each container runs in its own little world and we have to do some work to expose our API to our user. With a library, the consumer can just call a Python function, pass in some inputs and get back some outputs. With a container, our model is more like an app or service, so there’s no built-in way for a consumer to “call” the container with some input data and get back some output data.&lt;/p&gt;

&lt;p&gt;One option is to create a command line interface, but that requires explicitly binding input and output files to the container. This feels a bit unnatural, but we can see an example of it in &lt;a href="https://hub.docker.com/r/dpokidov/imagemagick" rel="noopener noreferrer"&gt;this container image of ImageMagick&lt;/a&gt; by dpokidov. In the “Usage” section, the author recommends binding a local folder into the container in order to run it as a command line app.&lt;/p&gt;

&lt;p&gt;The traditional answer to this question for Docker images is to expose an HTTP-based API, which is what Gravity AI’s containers do. But this means turning our function (e.g. &lt;code&gt;classifier()&lt;/code&gt;) into a service, which means we need to figure out where to put this service. To give the traditional answer again, we could deploy it to Kubernetes with an autoscaler and a load balancer in front of it, which can work well if e.g. you have a constant stream of processes that need to call &lt;code&gt;classifier()&lt;/code&gt;. But you might instead have a use case where some data shows up from a vendor every few hours which triggers a batch processing job. In that case, things can get a bit weird. You might be in a situation where the batch job is running, calls &lt;code&gt;classifier()&lt;/code&gt;, then has to wait for the autoscaler/Kubernetes to find a free machine that can spin up the service while the machine running the batch job is sitting idle.&lt;/p&gt;

&lt;p&gt;In other words, both of these options (library vs service) are reasonable, but they come with their own disadvantages.&lt;/p&gt;

&lt;p&gt;As a bit of an aside, you could imagine a way to get the best of both worlds with an extension to Docker that would allow you to publish a container that exposes a Python API, so that someone could call &lt;code&gt;sentiment = call_container_api(image="huggingface/transformers", "my input text")&lt;/code&gt; directly from their python code. This would effectively be a remote procedure call into a container that is &lt;em&gt;not&lt;/em&gt; running as a service but instead spun up just for the purpose of executing a function on-demand. This feels like a really heavyweight approach to solving dependency hell, but if your libraries are using a cross-platform memory format (hello &lt;a href="https://arrow.apache.org/" rel="noopener noreferrer"&gt;Apache Arrow&lt;/a&gt;!) under the covers, you could imagine doing some fun tricks like giving the container a read-only view into the caller’s memory space to reduce the overhead. It’s a bit implausible, but sometimes it’s helpful to sketch out these ideas to clarify the tradeoffs we’re making with the more practical bits of technology we have available.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using models-in-containers locally
&lt;/h4&gt;

&lt;p&gt;In this article, we’ll tackle this batch jobs-with-containers scenario. To make this concrete, let’s say that every morning, a vendor gives us a relatively large dump of everything everyone has said on the internet (Twitter, Reddit, Seeking Alpha, etc.) about the companies in the S&amp;amp;P 500 overnight. We want to feed these pieces of text into &lt;a href="https://github.com/ProsusAI/finBERT" rel="noopener noreferrer"&gt;FinBERT&lt;/a&gt;, which is a version of &lt;a href="https://arxiv.org/pdf/1810.04805.pdf" rel="noopener noreferrer"&gt;BERT&lt;/a&gt; that has been &lt;a href="https://flyyufelix.github.io/2016/10/03/fine-tuning-in-keras-part1.html" rel="noopener noreferrer"&gt;fine-tuned&lt;/a&gt; for financial sentiment analysis. BERT is a &lt;a href="https://en.wikipedia.org/wiki/BERT_(language_model)" rel="noopener noreferrer"&gt;language model&lt;/a&gt; from Google that was state of the art when it was published in 2018.&lt;/p&gt;

&lt;p&gt;We’ll be using Gravity AI’s &lt;a href="https://www.gravity-ai.com/catalog/product/dc7843d9-f2eb-4d71-aeb8-8607e646545a" rel="noopener noreferrer"&gt;container for FinBERT&lt;/a&gt;, but we’ll also assume that we operate in a primarily batch process-oriented environment, so we don’t have e.g. a Kubernetes cluster set up and even if we did it would probably be tricky to get the autoscaling right because of our usage pattern.&lt;/p&gt;

&lt;p&gt;If we’re just trying this out on our local machine, it’s pretty straightforward to use, as per &lt;a href="https://docs.gravity-ai.com/using-containers#running-the-container-locally" rel="noopener noreferrer"&gt;Gravity AI’s documentation&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker load &lt;span class="nt"&gt;-i&lt;/span&gt; Sentiment_Analysis_o_77f77f.docker.tar.gz
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 7000:80 gx-images:t-39c447b9e5b94d7ab75060d0a927807f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Sentiment_Analysis_o_77f77f.docker.tar.gz&lt;/code&gt; is the name of the file you download from Gravity AI and &lt;code&gt;gx-images:t-39c447b9e5b94d7ab75060d0a927807f&lt;/code&gt; is the name of the Docker image once it’s loaded from the .tar.gz file, which will show up in the output of the first command.&lt;/p&gt;

&lt;p&gt;And then we can write a bit of glue code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import time

import requests


def upload_license_file(base_url: str) -&amp;gt; None:
    with open("Sentiment Analysis on Financial Text.gravity-ai.key", "r") as f:
        response = requests.post(f"{base_url}/api/license/file", files={"License": f})
        response.raise_for_status()


def call_finbert_container(base_url: str, input_text: str) -&amp;gt; str:
    add_job_response = requests.post(
        f"{base_url}/data/add-job",
        files={
            "CallbackUrl": (None, ""),
            "File": ("temp.txt", input_text, "text/plain"),
            "MimeType": (None, "text/plain"),
        }
    )
    add_job_response.raise_for_status()
    add_job_response_json = add_job_response.json()
    if (
        add_job_response_json.get("isError", False)
        or add_job_response_json.get("errorMessage") is not None
    ):
        raise ValueError(f"Error from server: {add_job_response_json.get('errorMessage')}")
    job_id = add_job_response_json["data"]["id"]
    job_status = add_job_response_json["data"]["status"]

    while job_status != "Complete":
        status_response = requests.get(f"{base_url}/data/status/{job_id}")
        status_response.raise_for_status()
        job_status = status_response.json()["data"]["status"]
        time.sleep(1)

    result_response = requests.get(f"{base_url}/data/result/{job_id}")
    return result_response.text


def process_data():
    base_url = "http://localhost:7000"

    upload_license_file(base_url)

    # Pretend to query input data from somewhere
    sample_data = [
        "Finnish media group Talentum has issued a profit warning",
        "The loss for the third quarter of 2007 was EUR 0.3 mn smaller than the loss of"
        " the second quarter of 2007"
    ]

    results = [call_finbert_container(base_url, line) for line in sample_data]

    # Pretend to store the output data somewhere
    print(results)


if __name__ == "__main__":
    process_data()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;call_finbert_container&lt;/code&gt; calls the REST API provided by the container to submit a job, polls for the job completion, and then returns the job result. &lt;code&gt;process_data&lt;/code&gt; pretends to get some text data and processes it using our container, and then pretends to write the output somewhere else. We’re also assuming you’ve downloaded the Gravity AI key to the current directory as &lt;code&gt;Sentiment Analysis on Financial Text.gravity-ai.key&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using models-in-containers on the cloud
&lt;/h4&gt;

&lt;p&gt;This works great for playing around with our model locally, but at some point we’ll probably want to run this on the cloud, either to access additional compute whether that’s CPU or GPU, or to run this as a scheduled job with e.g. Airflow. The usual thing to do is package up our code (finbert_local_example.py and its dependencies) as a container, which means now we have two containers — one containing our glue code, and the FinBERT container that we need to launch together and coordinate (i.e. our glue code container needs to know the address/name of the FinBERT container to access it). We might start reaching for &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; which works great for long-running services, but in the context of an ad-hoc distributed batch job or a scheduled job, it will be tricky to work with.&lt;/p&gt;

&lt;p&gt;Instead, we’ll use &lt;a href="https://meadowrun.io/" rel="noopener noreferrer"&gt;Meadowrun&lt;/a&gt; to do most of the heavy lifting. Meadowrun will not only take care of the usual difficulties of allocating instances, deploying our code, etc., but also help launch an extra container and make it available to our code.&lt;/p&gt;

&lt;p&gt;To follow along, you’ll need to set up an environment. We’ll show how this works with pip on Windows, but you should be able to follow along the package manager of your choice (conda environments don’t work across platforms, so conda will only work if you’re on Linux).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m venv venv
venv\Scripts\activate.bat
pip install requests meadowrun
meadowrun-manage-ec2 install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a new virtaulenv, adds requests and meadowrun, and then installs Meadowrun into your AWS account.&lt;/p&gt;

&lt;p&gt;When you download a container from Gravity AI, it comes as a .tar.gz file that needs to get uploaded to a container registry in order to work. There are slightly longer instructions in the Gravity AI documentation, but here’s a short version of how to create an &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;ECR&lt;/a&gt; (Elastic Container Registry) repository then upload a container from Gravity AI to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; mygravityaiaws ecr get-login-password | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; 012345678901.dkr.ecr.us-east-2.amazonaws.comdocker tag gx-images:t-39c447b9e5b94d7ab75060d0a927807f 012345678901.dkr.ecr.us-east-2.amazonaws.com/mygravityai:finbertdocker push 012345678901.dkr.ecr.us-east-2.amazonaws.com/mygravityai:finbert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;012345678901&lt;/code&gt; appears in a few places in this snippet and it needs to be replaced with your account id. You’ll see your account id in the output of the first command as &lt;code&gt;registryId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more step before we can run some code: we’ll need to give the Meadowrun role permissions to this ECR repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;meadowrun-manage-ec2 grant-permission-to-ecr-repo mygravityai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can run our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;finbert_local_example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;upload_license_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_finbert_container&lt;/span&gt;

&lt;span class="c1"&gt;# Normally this would live in S3 or a database somewhere
&lt;/span&gt;&lt;span class="n"&gt;_SAMPLE_DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Company1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Commission income fell to EUR 4.6 mn from EUR 5.1 mn in the corresponding &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;period in 2007&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The purchase price will be paid in cash upon the closure of the transaction, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduled for April 1 , 2009&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Company2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The loss for the third quarter of 2007 was EUR 0.3 mn smaller than the loss of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; the second quarter of 2007&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consolidated operating profit excluding one-off items was EUR 30.6 mn, up from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; EUR 29.6 mn a year earlier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_one_company&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://container-service-0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;data_for_company&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_SAMPLE_DATA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nf"&gt;upload_license_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://container-service-0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;call_finbert_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data_for_company&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;process_one_company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Company1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Company2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EC2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mirror_local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;working_directory_globs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;sidecar_containers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ContainerInterpreter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;012345678901.dkr.ecr.us-east-2.amazonaws.com/mygravityai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finbert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;process_data&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s walk through the &lt;code&gt;process_data&lt;/code&gt; function line by line.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Meadowrun’s &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.run_map" rel="noopener noreferrer"&gt;run_map&lt;/a&gt; function runs the specified function (in this case &lt;code&gt;process_one_company&lt;/code&gt;) in parallel on the cloud. In this case, we provide two arguments (&lt;code&gt;["Company1", "Company2"]&lt;/code&gt;) so we’ll run these two tasks in parallel. The idea is that we’re splitting up the workload so that we can finish the job quickly.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.AllocCloudInstance" rel="noopener noreferrer"&gt;AllocCloudInstance&lt;/a&gt; tells Meadowrun to launch an EC2 instance to run this job if we don’t have one running already and &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.Resources" rel="noopener noreferrer"&gt;Resources&lt;/a&gt; tells Meadowrun what resources are needed to run this code. In this case we’re requesting 1 CPU and 1.5 GB of RAM per task. We’re also specifying that we we’re okay with spot instances up to an 80% eviction rate (aka probability of interruption).&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.run_job.Deployment.mirror_local" rel="noopener noreferrer"&gt;mirror_local&lt;/a&gt; tells Meadowrun that we want to use the code in the current directory, which is important as we’re reusing some code from finbert_local_example.py. Meadowrun only uploads .py files by default, but in our case, we need to include the .key file in our current working directory so that we can apply the Gravity AI license.&lt;/li&gt;
&lt;li&gt;  Finally, container_services tells Meadowrun to launch a container with the specified image for every task we have running in parallel. Each task can access its associated container as &lt;code&gt;container-service-0&lt;/code&gt;, which you can see in the code for &lt;code&gt;process_one_company&lt;/code&gt;. If you’re following along, you’ll need to edit the account id again to match your account id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at a few of the more important lines from the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launched 1 new instance(s) (total $0.0209/hr) for the remaining 2 workers:
 ec2-13-59-48-22.us-east-2.compute.amazonaws.com: r5d.large (2.0 CPU, 16.0 GB), spot ($0.0209/hr, 2.5% chance of interruption), will run 2 workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here Meadowrun is telling us that it’s launching the cheapest EC2 instance that can run our job. In this case, we’re only paying 2¢ per hour!&lt;/p&gt;

&lt;p&gt;Next, Meadowrun will replicate our local environment on the EC2 instance by building a new container image, and then also pull the FinBERT container that we specified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Building python environment in container  4e4e2c...
...
Pulling docker image 012345678901.dkr.ecr.us-east-2.amazonaws.com/mygravityai:finbert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our final result will be some output from the FinBERT model that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[['sentence,logit,prediction,sentiment_score\nCommission income fell to EUR 4.6 mn from EUR 5.1 mn in the corresponding period in 2007,[0.24862055 0.44351986 0.30785954],negative,-0.1948993\n',
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Closing remarks
&lt;/h4&gt;

&lt;p&gt;Gravity AI packages up ML models as containers which are really easy to use as services. Making these containers work naturally for batch jobs takes a bit of work, but Meadowrun makes it easy!&lt;/p&gt;

</description>
      <category>mlops</category>
      <category>python</category>
      <category>docker</category>
    </item>
    <item>
      <title>Run Your Own DALL·E Mini (Craiyon) Server on EC2</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Tue, 26 Jul 2022 15:53:00 +0000</pubDate>
      <link>https://dev.to/meadowrun/run-your-own-dalle-mini-craiyon-server-on-ec2-1pjh</link>
      <guid>https://dev.to/meadowrun/run-your-own-dalle-mini-craiyon-server-on-ec2-1pjh</guid>
      <description>&lt;p&gt;In case you’ve been under a rock for the last few months, &lt;a href="https://openai.com/blog/dall-e/" rel="noopener noreferrer"&gt;DALL·E&lt;/a&gt; is an ML model from OpenAI that &lt;a href="https://www.instagram.com/openaidalle/" rel="noopener noreferrer"&gt;generates&lt;/a&gt; &lt;a href="https://qz.com/2176389/the-best-examples-of-dall-e-2s-strange-beautiful-ai-art/" rel="noopener noreferrer"&gt;images&lt;/a&gt; from text prompts. &lt;a href="https://github.com/borisdayma/dalle-mini" rel="noopener noreferrer"&gt;DALL·E Mini&lt;/a&gt; (renamed to Craiyon) by Boris Dayma et al. is a less powerful but open version of DALL·E, and there’s a hosted version at &lt;a href="https://www.craiyon.com/" rel="noopener noreferrer"&gt;craiyon.com&lt;/a&gt; for everyone to try.&lt;/p&gt;

&lt;p&gt;If you’re anything like us, though, you’ll feel compelled to poke around the code and run the model yourself. We’ll do that in this article using &lt;a href="https://meadowrun.io/" rel="noopener noreferrer"&gt;Meadowrun&lt;/a&gt;, an open-source library that makes it easy to run Python code in the cloud. For ML models in particular, we just added a feature for requesting GPU machines in a &lt;a href="https://github.com/meadowdata/meadowrun/releases/tag/v0.1.14" rel="noopener noreferrer"&gt;recent release&lt;/a&gt;. We’ll also feed the images generated by DALL·E Mini into additional image processing models (&lt;a href="https://github.com/Jack000/glid-3-xl" rel="noopener noreferrer"&gt;GLID-3-xl&lt;/a&gt; and &lt;a href="https://github.com/JingyunLiang/SwinIR" rel="noopener noreferrer"&gt;SwinIR&lt;/a&gt;) to improve the quality of our generated images. Along the way we’ll deal with the speedbumps that come up when running open-source ML models on EC2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running dalle-playground
&lt;/h2&gt;

&lt;p&gt;For the first half of this article, we’ll show how to run &lt;a href="https://github.com/saharmor/dalle-playground" rel="noopener noreferrer"&gt;saharmor/dalle-playground&lt;/a&gt;, which wraps the DALL·E Mini code in an HTTP API, and provides a simple web page to generate images via that API.&lt;/p&gt;

&lt;p&gt;dalle-playground provides a &lt;a href="https://colab.research.google.com/github/saharmor/dalle-playground/blob/main/backend/dalle_playground_backend.ipynb" rel="noopener noreferrer"&gt;Jupyter notebook&lt;/a&gt; that you can run in Google Colab. If you’re doing anything more than kicking the tires, though, you’ll run into the &lt;a href="https://stackoverflow.com/questions/61126851/how-can-i-use-gpu-on-google-colab-after-exceeding-usage-limit" rel="noopener noreferrer"&gt;dynamic usage limit&lt;/a&gt; in Colab’s free tier. You could upgrade to Colab Pro ($9.99/month) or Colab Pro+ ($49.99/month), but we’ll get this functionality for pennies on the dollar by using AWS directly!&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;First, you’ll need an AWS account. If you’ve never used GPU instances in AWS before, you’ll probably need to increase your quotas. AWS accounts have quotas in each region that limit how many CPUs of a particular instance type you can run at once. There are 4 quotas for GPU instances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  L-3819A6DF: “&lt;a href="https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas/L-3819A6DF" rel="noopener noreferrer"&gt;All G and VT Spot Instance Requests&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;  L-7212CCBC: “&lt;a href="https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas/L-7212CCBC" rel="noopener noreferrer"&gt;All P Spot Instance Requests&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;  L-DB2E81BA: “&lt;a href="https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas/L-DB2E81BA" rel="noopener noreferrer"&gt;Running On-Demand G and VT instances&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;  L-417A185B: “&lt;a href="https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas/L-417A185B" rel="noopener noreferrer"&gt;Running On-Demand P instances&lt;/a&gt;”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all set to 0 for a new EC2 account, so if you try to run the code below, you’ll get this message from Meadowrun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to launch new g4dn.xlarge spot instances due to the L-3819A6DF
quota which is set to 0. This means you cannot have more than 0 CPUs
across all of your spot instances from the g, vt instance families.
This quota is currently met. Run `aws service-quotas
request-service-quota-increase --service-code ec2 --quota-code
L-3819A6DF --desired-value X` to set the quota to X, where X is
larger than the current quota. (Note that terminated instances
sometimes count against this limit:
https://stackoverflow.com/a/54538652/908704 Also, quota increases are
not granted immediately.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We recommend running the command in that message or clicking on one of the links in the list above to request a quota increase if you’re giving this a go (if you use a link, make sure you are in the same region as your AWS CLI as given by &lt;code&gt;aws configure get region&lt;/code&gt;). It seems like AWS has a human in the loop for granting quota increases, and in our experience it can take up to a day or two to get a quota increase granted.&lt;/p&gt;

&lt;p&gt;Second, we’ll need a local Python environment with Meadowrun, and then we’ll install Meadowrun in our AWS account. Here’s an example using pip in Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv meadowrun-venv
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;meadowrun-venv/bin/activate
&lt;span class="nv"&gt;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;meadowrun
&lt;span class="nv"&gt;$ &lt;/span&gt;meadowrun-manage-ec2 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--allow-authorize-ips&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running DALL·E Mini
&lt;/h3&gt;

&lt;p&gt;Now that we have that out of the way, it’s easy to run the dalle-playground backend!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_dallemini&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python backend/app.py --port 8080 --model_version mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EC2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;gpu_memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;git_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/hrichardlee/dalle-playground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PipRequirementsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/requirements.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_dallemini&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick tour of this snippet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;run_command&lt;/code&gt; tells Meadowrun to run &lt;code&gt;python backend/app.py --port 8080 --model_version mini&lt;/code&gt; on an EC2 instance. This starts the dalle-playground backend on port 8080, using the mini version of DALL·E Mini. The &lt;a href="https://huggingface.co/dalle-mini/dalle-mini" rel="noopener noreferrer"&gt;mini version&lt;/a&gt; is 27 times smaller than the &lt;a href="https://huggingface.co/dalle-mini/dalle-mega" rel="noopener noreferrer"&gt;mega version&lt;/a&gt; of DALL·E Mini, which makes it less powerful but easier to run.&lt;/li&gt;
&lt;li&gt;  The next few lines tell Meadowrun what the requirements for our job are: 1 CPU, 16 GB of main memory, and we’re okay with spot instances up to an 80% probability of eviction (aka interruption). The instance types we’ll be using do tend to get interrupted, so if that becomes a problem we can change this to 0% which tells Meadowrun we want an on-demand instance. We also ask for an Nvidia GPU that has at least 4GB of GPU memory which is what’s needed by the mini model.&lt;/li&gt;
&lt;li&gt;  Next, we want the code in the &lt;a href="https://github.com/hrichardlee/dalle-playground" rel="noopener noreferrer"&gt;https://github.com/hrichardlee/dalle-playground&lt;/a&gt; repo, and we want to construct a pip environment from the backend/requirements.txt file in that repo. We were almost able to use the &lt;a href="https://github.com/saharmor/dalle-playground" rel="noopener noreferrer"&gt;saharmor/dalle-playground&lt;/a&gt; repo as-is, but we had to make &lt;a href="https://github.com/hrichardlee/dalle-playground/commit/85ecaf866ab74c58295fb0f91846b4bb4326f04b" rel="noopener noreferrer"&gt;one change&lt;/a&gt; to add the jax[cuda] package to the requirements.txt file. In case you haven’t seen &lt;a href="https://github.com/google/jax" rel="noopener noreferrer"&gt;jax&lt;/a&gt; before, jax is a machine-learning library from Google, roughly equivalent to Tensorflow or PyTorch. It combines &lt;a href="https://github.com/HIPS/autograd" rel="noopener noreferrer"&gt;Autograd&lt;/a&gt; for automatic differentiation and &lt;a href="https://www.tensorflow.org/xla" rel="noopener noreferrer"&gt;XLA&lt;/a&gt; (accelerated linear algebra) for JIT-compiling numpy-like code for Google’s TPUs or Nvidia’s CUDA API for GPUs. The CUDA support requires explicitly selecting the [cuda] option when we install the package.&lt;/li&gt;
&lt;li&gt;  Finally, we tell Meadowrun that we want to open port 8080 on the machine that’s running this job so that we can access the backend from our current IP address. Be careful with this! dalle-playground doesn’t use TLS and it’s not a good idea to give everyone with your IP address access to this interface forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To walk through selected parts of the output from this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launched a new instance for the job:
ec2-3-138-184-193.us-east-2.compute.amazonaws.com: g4dn.xlarge (4.0
CPU, 16.0 GB, 1.0 GPU), spot ($0.1578/hr, 61.0% chance of
interruption), will run 1 workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here Meadowrun tells us everything we need to know about the instance it started for this job and how much it will cost us (only 15¢ per hour!).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Building python environment in container  eccac6...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, Meadowrun is building a container based on the contents of the requirements.txt file we specified. This takes a while, but Meadowrun caches the image in &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;ECR&lt;/a&gt; for you so this only needs to happen once (until your requirements.txt file changes). Meadowrun also cleans up the image if you don’t use it for a while.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\--&amp;gt; Starting DALL-E Server. This might take up to two minutes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we’ve gotten to the code in dalle-playground, which needs to do a few minutes of initialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\--&amp;gt; DALL-E Server is up and running!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we’re up and running!&lt;/p&gt;

&lt;p&gt;Now we’ll need to run the front end on our local machine (if you don’t have npm, you’ll need to install &lt;a href="https://nodejs.org/en/download/" rel="noopener noreferrer"&gt;node.js&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/saharmor/dalle-playground
cd dalle-playground/interface
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll want to construct the backend URL in a separate editor, e.g. &lt;code&gt;http://ec2-3-138-184-193.us-east-2.compute.amazonaws.com:8080&lt;/code&gt; and copy/paste it into the webapp—typing it in directly causes unnecessary requests to the partially complete URL, which fail slowly.&lt;/p&gt;

&lt;p&gt;Time to generate some images!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AxDpc4pYAIALZxG-_P575tw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AxDpc4pYAIALZxG-_P575tw.png" alt="Images generated by DALL·E Mini for batman praying in the garden of gethsemane"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mini (mini version): batman praying in the garden of gethsemane
  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A6hKd9nkzc3TsNxMpvwin2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A6hKd9nkzc3TsNxMpvwin2w.png" alt="Images generated by DALL·E Mini for olive oil and vinegar drizzled on a plate in the shape of the solar system"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mini (mini version): olive oil and vinegar drizzled on a plate in the shape of the solar system
  &lt;/p&gt;

&lt;p&gt;It was pretty easy to get this working, but this model isn’t really doing what we’re asking it to do. For the first set of images, we clearly have a Batman-like figure, but he’s not really praying and I’m not sure he’s in the garden of Gethsemane. For the second set of images, it looks like we’re either getting olive oil or a planet, but we’re not getting both of them in the same image, let alone an entire system. Let’s see if the “mega” version of DALL·E Mini can do any better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running DALL·E Mega
&lt;/h3&gt;

&lt;p&gt;DALL·E Mega is a larger version of DALL·E Mini, meaning it has the same architecture but more parameters. Theoretically we can just replace &lt;code&gt;--model_version mini&lt;/code&gt; with &lt;code&gt;--model_version mega_full&lt;/code&gt; in the previous snippet and get the mega version. When we do this, though, the dalle-playground initialization code takes about 45 minutes.&lt;/p&gt;

&lt;p&gt;We don’t need any real profiling to figure this one out—if you just kill the process after it’s running for a while, the stack trace will clearly show that the culprit is the &lt;a href="https://github.com/borisdayma/dalle-mini/blob/ec07a902e440077efc84dde401ac6f65af5c0a09/src/dalle_mini/model/utils.py#L14" rel="noopener noreferrer"&gt;from_pretrained&lt;/a&gt; function, which is downloading the pretrained model weights from Weights and Biases (aka wandb). Weights and Biases is an MLOps platform that helps you keep track of the code, data, and analyses that go into training and evaluating an ML model. For the purposes of this article, it’s where we go to download pretrained model weights. We can look at the &lt;a href="https://github.com/saharmor/dalle-playground/blob/da74e4580d881b0a72f21578b3fca35735d9d166/backend/consts.py#L5" rel="noopener noreferrer"&gt;specification for the artifacts&lt;/a&gt; we’re downloading from wandb, browse to the &lt;a href="https://wandb.ai/dalle-mini/dalle-mini/artifacts/DalleBart_model/mega-1/v16/files" rel="noopener noreferrer"&gt;web view&lt;/a&gt; for the mega version and see that the main file we need is about 10GB. If we &lt;a href="https://docs.meadowrun.io/en/stable/how_to/ssh_to_instance/" rel="noopener noreferrer"&gt;ssh into the EC2 instance&lt;/a&gt; that Meadowrun creates to run this command and run &lt;a href="https://www.linuxjournal.com/content/sysadmins-toolbox-iftop" rel="noopener noreferrer"&gt;iftop&lt;/a&gt;, we can see that we’re getting a leisurely 35 Mbps from wandb.&lt;/p&gt;

&lt;p&gt;We don’t want to wait 45 minutes every time we run DALL-E Mega, and it’s painful to see a powerful GPU machine sipping 35 Mbps off the internet while almost all of its resources sit idle. So we made &lt;a href="https://github.com/hrichardlee/dalle-playground/commit/3c39840b55964ac01f47f39c8340b4bcea52a6d6" rel="noopener noreferrer"&gt;some tweaks&lt;/a&gt; to dalle-playground to cache the artifacts in S3. &lt;a href="https://github.com/hrichardlee/dalle-playground/blob/s3cache/backend/cache_in_s3.py" rel="noopener noreferrer"&gt;cache_in_s3.py&lt;/a&gt; effectively calls &lt;code&gt;wandb.Api().artifact("dalle-mini/dalle-mini/mega-1:latest").download()&lt;/code&gt; then uploads the artifacts to S3. To follow along, you’ll first need to create an S3 bucket and give the Meadowrun EC2 role access to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 mb s3://meadowrun-dallemini
meadowrun-manage-ec2 grant-permission-to-s3-bucket meadowrun-dallemini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that S3 bucket names need to be globally unique, so you won’t be able to use the exact same name we’re using here.&lt;/p&gt;

&lt;p&gt;Then we can use Meadowrun to kick off the long-running download job on a much cheaper machine—note that we’re only requesting 2 GB of memory and no GPUs for this job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cache_pretrained_model_in_s3&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python backend/cache_in_s3.py --model_version mega_full --s3_bucket meadowrun-dallemini --s3_bucket_region us-east-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EC2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;git_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/hrichardlee/dalle-playground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PipRequirementsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/requirements_for_caching.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cache_pretrained_model_in_s3&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve also &lt;a href="https://github.com/hrichardlee/dalle-playground/compare/main...s3cache" rel="noopener noreferrer"&gt;changed the model code&lt;/a&gt; to download files from S3 instead of wandb. We’re downloading the files into the special &lt;code&gt;/var/meadowrun/machine_cache&lt;/code&gt; folder which is shared across Meadowrun-launched containers on a machine. That way, if we run the same container multiple times on the same machine, we won’t need to redownload these files.&lt;/p&gt;

&lt;p&gt;Once that’s in place, we can run the mega version and it will start up relatively quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_dallemega&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python backend/app.py --port 8080 --model_version mega_full --s3_bucket meadowrun-dallemini --s3_bucket_region us-east-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EC2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpu_memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;git_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/hrichardlee/dalle-playground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;interpreter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PipRequirementsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/requirements.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_dallemega&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note about this snippet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We’re asking Meadowrun to use the &lt;code&gt;s3cache&lt;/code&gt; branch of our git repo, which includes the changes to allow caching/retrieving the artifacts in S3.&lt;/li&gt;
&lt;li&gt;  We’ve increased the requirements to 32 GB of main memory and 12 GB of GPU memory, which the larger model requires.&lt;/li&gt;
&lt;li&gt;  The first time we run, Meadowrun builds a new image because we added the boto3 package for fetching our cached files from S3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One last note—Meadowrun’s &lt;code&gt;install&lt;/code&gt; sets up an AWS Lambda that runs periodically and cleans up your instances automatically if you haven’t run a job for a while. To be extra safe, you can also manually clean up instances with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meadowrun-manage-ec2 clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what we get:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AgeG0YcfWAOy2woxKKXuknQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AgeG0YcfWAOy2woxKKXuknQ.png" alt="Images generated by DALL·E Mini for batman praying in the garden of gethsemane"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mega (full version of DALL·E Mini): batman praying in the garden of gethsemane
  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2ACFlG_YnqLyUJr9UsyCaV3A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2ACFlG_YnqLyUJr9UsyCaV3A.png" alt="Images generated by DALL·E Mini for olive oil and vinegar drizzled on a plate in the shape of the solar system"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mega (full version of DALL·E Mini): olive oil and vinegar drizzled on a plate in the shape of the solar system
  &lt;/p&gt;

&lt;p&gt;Much better! For the first set of images, I’m not sure Batman is praying in all of those images, but he’s definitely Batman and he’s definitely in the garden of Gethsemane. For the second set of images, we have a plate now, some olive oil and vinegar, and it definitely looks like more of a solar system. The images aren’t quite on par with OpenAI’s DALL·E yet, but they are noticeably better! Unfortunately there’s not too much more we can do to improve the translation of text to image short of training our own 12 billion-parameter model, but we’ll try tacking on a diffusion model to improve the finer details in the images. We’ll also add a model for upscaling the images, as they’re only 256x256 pixels right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an image generation pipeline
&lt;/h2&gt;

&lt;p&gt;For the second half of this article, we’ll use &lt;a href="https://github.com/meadowdata/meadowrun-dallemini-demo" rel="noopener noreferrer"&gt;meadowdata/meadowrun-dallemini-demo&lt;/a&gt; which contains a notebook for running multiple models as sequential batch jobs to generate images using Meadowrun. The combination of models is inspired by &lt;a href="https://github.com/jina-ai/dalle-flow" rel="noopener noreferrer"&gt;jina-ai/dalle-flow&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  DALL·E Mini: The model we’ve been focusing on in the first half of this article. &lt;a href="https://ml.berkeley.edu/blog/posts/dalle2/" rel="noopener noreferrer"&gt;This post&lt;/a&gt; is a good guide to how OpenAI’s DALL·E 2 is built. To simplify, DALL·E is a combination of two models. The first model is trained on images and learns how to “compress” images to vectors and then “decompress” those vectors back into the original images. The second model is trained on image/caption pairs and learns how to turn captions into image vectors. After training, we can put new captions into the second model to produce an image vector, and then we can feed that image vector into the first model to produce a novel image.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/Jack000/glid-3-xl" rel="noopener noreferrer"&gt;GLID-3-xl&lt;/a&gt;: A diffusion model. &lt;a href="https://huggingface.co/blog/annotated-diffusion" rel="noopener noreferrer"&gt;Diffusion models&lt;/a&gt; are trained by taking images, blurring (aka diffusing) them, and training the model on original/blurred image pairs. The model learns to reconstruct the original unblurred version from the blurred version. Diffusion models can be used for a variety of tasks, but in this case we’ll use GLID-3-xl to fill in the finer details in our images.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/JingyunLiang/SwinIR" rel="noopener noreferrer"&gt;SwinIR&lt;/a&gt;: A model for upscaling images (aka image restoration). Image restoration models are trained by taking images and downscaling them. The model learns to produce the original higher resolution image from the downscaled image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To run this pipeline, in addition to the prerequisites from the first half of this article, we’ll get the meadowrun-dallemini-demo git repo and the local dependencies, then launch a Jupyter notebook server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/meadowdata/meadowrun-dallemini-demo
cd meadowrun-dallemini-demo
# assuming you are already in a virtualenv from before
pip install -r local_requirements.txt
jupyter notebook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll then need to open the &lt;a href="https://github.com/meadowdata/meadowrun_dallemini_demo/blob/main/meadowrun-image-generation.ipynb" rel="noopener noreferrer"&gt;main notebook&lt;/a&gt; in Jupyter, and edit &lt;code&gt;S3_BUCKET_NAME&lt;/code&gt; and &lt;code&gt;S3_BUCKET_REGION&lt;/code&gt; to match the bucket we created in the first half of this article.&lt;/p&gt;

&lt;p&gt;The code in the notebook is similar to the first half of this article so we won’t go over it in depth. A few notes on what the rest of the code in the repo is doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We’ve adapted the sample code that comes with all of our models to use our S3 cache and provide easy-to-use interfaces in &lt;a href="https://github.com/meadowdata/meadowrun-dallemini-demo/blob/main/linux/dalle_wrapper.py" rel="noopener noreferrer"&gt;dalle_wrapper.py&lt;/a&gt;, &lt;a href="https://github.com/meadowdata/meadowrun-dallemini-demo/blob/main/linux/glid3xl_wrapper.py" rel="noopener noreferrer"&gt;glid3xl_wrapper.py&lt;/a&gt;, and &lt;a href="https://github.com/meadowdata/meadowrun-dallemini-demo/blob/main/linux/swinir_wrapper.py" rel="noopener noreferrer"&gt;swinir_wrapper.py&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  We’re referencing our three models directly as git repos (because they’re not available as packages in &lt;a href="https://pypi.org/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;) in &lt;a href="https://github.com/meadowdata/meadowrun-dallemini-demo/blob/main/model_requirements.txt" rel="noopener noreferrer"&gt;model_requirements.txt&lt;/a&gt;, but we had to make a few changes to make these repos work as pip packages. Pip looks for a setup.py file in the git repo to figure out which files from the repo need to be installed into the environment, as well as what the dependencies of that repo are. GLID-3-xl and &lt;a href="https://github.com/CompVis/latent-diffusion" rel="noopener noreferrer"&gt;latent-diffusion&lt;/a&gt; (another diffusion model that GLID-3-xl depends on) had setup.py files that &lt;a href="https://github.com/hrichardlee/glid-3-xl/commit/ffbdd0077c67ac7e7610e09886565c452bc5c71e" rel="noopener noreferrer"&gt;needed&lt;/a&gt; &lt;a href="https://github.com/hrichardlee/latent-diffusion/commit/d9f6b45f7dd5d5dca214f5803f4f3e72af7649a6" rel="noopener noreferrer"&gt;tweaks&lt;/a&gt; to include all of the code needed to run the models. SwinIR didn’t have a setup.py file at all, so we &lt;a href="https://github.com/hrichardlee/SwinIR/commit/ae43e3893bae0bc1a8102c45ac71cffacc02dca0" rel="noopener noreferrer"&gt;added one&lt;/a&gt;. Finally, all of these setup.py files needed additional dependencies, which we just added to the model_requirements.txt file.&lt;/li&gt;
&lt;li&gt;  All of these models are pretty challenging to run on anything other than Linux, which is why we’ve split out the local_requirements.txt from the model_requirements.txt. Even if you’re running on Windows or Mac, you shouldn’t have any trouble running this notebook—Meadowrun takes care of creating the hairy model environment on an EC2 instance running Linux.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a couple more notes on Meadowrun:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Now that because we’re running these models as batch jobs instead of as services, Meadowrun will reuse a single EC2 instance to run them.&lt;/li&gt;
&lt;li&gt;  If you’re feeling ambitious, you could even use &lt;a href="https://docs.meadowrun.io/en/stable/reference/apis/#meadowrun.run_map" rel="noopener noreferrer"&gt;meadowrun.run_map&lt;/a&gt; to run these models in parallel on multiple GPU machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s see some results!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The notebook asks for a text prompt and has DALL·E Mini generate 8 images:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AF7G4YGt7UEMtplp-cLMTCQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AF7G4YGt7UEMtplp-cLMTCQ.png" alt="Images generated by DALL·E Mini for batman praying in the garden of gethsemane"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mini: batman praying in the garden of gethsemane
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We select one of the images and GLID-3-xl produces 8 new images based on our chosen image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AIBUU0fBtEzLTlwbb3_D7Nw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2AIBUU0fBtEzLTlwbb3_D7Nw.png" alt="Images generated by GLID-3-xl based on image 6 above"&gt;&lt;/a&gt;&lt;br&gt;Images generated by GLID-3-xl based on image 6 above
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Finally, we select one of these images and have SwinIR upscale it from 256x256 to 1024x1024 pixels:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A28ZN2qx0QhjNYBCqsR0bGg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2A28ZN2qx0QhjNYBCqsR0bGg.png" alt="Image 3 from above upscaled by SwinIR"&gt;&lt;/a&gt;&lt;br&gt;Image 3 from above upscaled by SwinIR
  &lt;/p&gt;

&lt;p&gt;Not terrible, although we did provide some human help at each stage!&lt;/p&gt;

&lt;p&gt;Here’s what OpenAI’s DALL·E generates from the same prompt:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1550117634863030276-670" src="https://platform.twitter.com/embed/Tweet.html?id=1550117634863030276"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1550117634863030276-670');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1550117634863030276&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;And here’s one more comparison:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2Aj1G0Wo0wi0FWmW2vOipFLg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1400%2F1%2Aj1G0Wo0wi0FWmW2vOipFLg.png" alt="DALL·E Mini: “olive oil and vinegar drizzled on a plate in the shape of the solar system”, upscaled by SwinIR"&gt;&lt;/a&gt;&lt;br&gt;DALL·E Mini: “olive oil and vinegar drizzled on a plate in the shape of the solar system”, upscaled by SwinIR
  &lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1519295975839260675-245" src="https://platform.twitter.com/embed/Tweet.html?id=1519295975839260675"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1519295975839260675-245');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1519295975839260675&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;All of this underscores how impressive OpenAI’s DALL·E is. That said, DALL·E Mini is very fun to play with, is truly open, and will only get better as it &lt;a href="https://wandb.ai/dalle-mini/dalle-mini/reports/DALL-E-Mega-Training-Journal--VmlldzoxODMxMDI2" rel="noopener noreferrer"&gt;continues to train&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing remarks
&lt;/h2&gt;

&lt;p&gt;This post demonstrates how to use Meadowrun for running GPU computations like ML inference in EC2. Meadowrun takes care of details like finding the cheapest available GPU instance types, as well as making sure &lt;a href="https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html" rel="noopener noreferrer"&gt;CUDA&lt;/a&gt; and Nvidia’s &lt;a href="https://developer.nvidia.com/blog/gpu-containers-runtime/" rel="noopener noreferrer"&gt;Container Runtime&lt;/a&gt; (previously known as &lt;a href="https://developer.nvidia.com/blog/nvidia-docker-gpu-server-application-deployment-made-easy/" rel="noopener noreferrer"&gt;Nvidia Docker&lt;/a&gt;) are installed in the right places.&lt;/p&gt;

&lt;p&gt;It’s pretty cool that we can point Meadowrun to a repo like dalle-playground, tell it what resources it needs, and get it running with almost no fuss. One of the most annoying things in software is getting other people’s code to work, and it’s great to see that the Python and ML ecosystem have made a ton of progress in this regard. Thanks to better package management tools, MLOps tools like Hugging Face and Weights and Biases, as well as Meadowrun (if we do say so ourselves) it’s easier than ever to build on the work of others.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To stay updated on Meadowrun, star us on &lt;a href="https://github.com/meadowdata/meadowrun" rel="noopener noreferrer"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Why Starting a Fresh EC2 Instance and Running Python with Meadowrun Took Over a Minute</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Thu, 14 Jul 2022 13:55:36 +0000</pubDate>
      <link>https://dev.to/meadowrun/why-starting-python-on-a-fresh-ec2-instance-takes-over-a-minute-621</link>
      <guid>https://dev.to/meadowrun/why-starting-python-on-a-fresh-ec2-instance-takes-over-a-minute-621</guid>
      <description>&lt;h4&gt;
  
  
  And what we did about it
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://meadowrun.io"&gt;Meadowrun&lt;/a&gt; is a no-ceremony tool to run your Python code in the cloud that automates the boring stuff for you.&lt;/p&gt;

&lt;p&gt;Meadowrun checks if suitable EC2 machines are already running, and starts some if not; it packages up code and environments; logs into the EC2 machine via SSH and runs your code; and finally, gets the results back, as well as logs and output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I2jK81L5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AHkFmFWj7Y3VMAyJn" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I2jK81L5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AHkFmFWj7Y3VMAyJn" alt="Runner on a starting block" width="880" height="586"&gt;&lt;/a&gt;&lt;br&gt;&lt;em&gt;Photo by Braden Collum on Unsplash&lt;/em&gt;
  &lt;/p&gt;

&lt;p&gt;This can take between a few seconds in the warmest of warm starts to several minutes in the coldest start. For a warm start, a machine is already running, we have a container already built and local code is uploaded to S3. During a cold start, all these things need to happen before you get to see your Python code actually run.&lt;/p&gt;

&lt;p&gt;Nobody likes waiting, so we were keen to improve the wait time. This is the story of what we discovered along the way.&lt;/p&gt;

&lt;h4&gt;
  
  
  You don’t know what you don’t measure
&lt;/h4&gt;

&lt;p&gt;At the risk of sounding like a billboard for telemetry services, the first rule when you’re trying to optimize something is: measure where you’re spending time.&lt;/p&gt;

&lt;p&gt;It sounds absolutely trivial, but if I had a dollar for every time I didn’t follow my own advice, I’d have…well, at least 10 dollars.&lt;/p&gt;

&lt;p&gt;If you’re optimizing a process that just runs locally, and has just one thread, this is all pretty easy to do, and there are various tools for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;log timings&lt;/li&gt;
&lt;li&gt;attach a debugger and break once in a while&lt;/li&gt;
&lt;li&gt;profiling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people jump straight to profiling, but personally I like the hands-on approaches especially in cases where whatever it is you’re optimizing has a long total runtime.&lt;/p&gt;

&lt;p&gt;In any case, measuring what Meadowrun does is a bit more tricky. First, it starts processes on remote machines. Ideally, we’d have some kind of overview where activities running on one machine that start activities on another machine are linked appropriately.&lt;/p&gt;

&lt;p&gt;Second, much of meadowrun’s activity consists of calls to AWS APIs, connection requests or other types of I/O.&lt;/p&gt;

&lt;p&gt;Because of both these reasons, meadowrun is not particularly well-suited to profiling. So we’re left with print statements—but surely something better must exist already?&lt;/p&gt;

&lt;p&gt;Indeed it does. After some research online, I came across &lt;a href="https://eliot.readthedocs.io/en/stable/"&gt;Eliot&lt;/a&gt;, a library that allows you to log what it calls actions. Crucially, actions have a beginning and end, and Eliot keeps track of which actions are part of other, higher-level actions. Additionally, it can do this across async, thread and process boundaries.&lt;/p&gt;

&lt;p&gt;The way it works is pretty simple: you can annotate functions or methods with a decorator which turns it into an Eliot-tracked action. For finer-grained actions, you can also put any code block into a context manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;eliot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_call&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;choose_instances&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;eliot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"subaction of choose"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this work across processes, you also need to pass an Eliot identifier to the other process, and put it in context there—just two extra lines of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# parent process
&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eliot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_action&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;serialize_task_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# child process receives task_id somehow, 
# e.g. via a command line argument
&lt;/span&gt;&lt;span class="n"&gt;eliot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;continue_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding the Eliot actions, Eliot logs information to a JSON file. You can then visualize it using a handy command line program called eliot-tree. Here’s the result of eliot-tree showing what Meadowrun does when calling &lt;code&gt;run_function&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kyndIArn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AWI_gVQGTOzfB14ZAqbXE-g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kyndIArn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2AWI_gVQGTOzfB14ZAqbXE-g.png" alt="A console window showing functions, code blocks, their relation and how long each took." width="880" height="614"&gt;&lt;/a&gt;&lt;br&gt;&lt;em&gt;Meadowrun being busy&lt;/em&gt;
  &lt;/p&gt;

&lt;p&gt;This is a great breakdown of how long everything takes, that pretty much directly led to all the investigations we’re about to discuss. For example, the whole run took 93 seconds, of which the job itself used only a handful of seconds—this was for a cold start situation. In this instance, it took about 15 seconds for AWS to report that our new instance is running and got an IP address (&lt;code&gt;wait_until_running&lt;/code&gt;) but then we had to wait another 34 seconds before we could actually SSH into the machine (&lt;code&gt;wait_for_ssh_connection&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Based on Eliot’s measurements, we tried to make improvements in a number of areas (but didn’t always succeed!).&lt;/p&gt;

&lt;h4&gt;
  
  
  Cache all the things some of the time
&lt;/h4&gt;

&lt;p&gt;One of the quickest wins: meadowrun was downloading EC2 instance prices before every run. In practice spot and on-demand prices don’t change frequently:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZtdPEebV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/799/1%2AL_qrNADOPS41OOBxBx-bQQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZtdPEebV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/799/1%2AL_qrNADOPS41OOBxBx-bQQ.png" alt="A screenshot of some EC2 spot price history." width="799" height="564"&gt;&lt;/a&gt;&lt;br&gt;&lt;em&gt;The patient has flatlined&lt;/em&gt;
  &lt;/p&gt;

&lt;p&gt;So it’s more reasonable to &lt;a href="https://github.com/meadowdata/meadowrun/pull/75"&gt;cache the download&lt;/a&gt; locally for up to 4 hours. That saves us 5–10 seconds on every run.&lt;/p&gt;

&lt;h4&gt;
  
  
  All Linuxes are fast but some are faster than others
&lt;/h4&gt;

&lt;p&gt;We can’t influence how long it takes for AWS to start a virtual machine, but perhaps some Linux distributions are faster to start than others—in particular it’d be nice if they’d start sshd sooner rather than later. That potentially cuts down on waiting for the ssh connection.&lt;/p&gt;

&lt;p&gt;Meadowrun is currently using an Ubuntu-based image, which according to our measurements takes about 30–40 seconds from the time when AWS says the machine is running, to when we can actually ssh into it. (This was measured while running from London to Ohio/us-east-2—if you run Meadowrun closer to home you’ll see better times.)&lt;/p&gt;

&lt;p&gt;In any case, according to &lt;a href="https://www.daemonology.net/blog/2021-08-12-EC2-boot-time-benchmarking.html"&gt;EC2 boot time benchmarking&lt;/a&gt;, some of which we independently verified, &lt;a href="https://clearlinux.org/"&gt;Clear Linux&lt;/a&gt; is the clear winner here. This is borne out in practice, reducing the “wait until ssh” time from 10 seconds to a couple when connecting to a close region, and from 30 seconds to 5 seconds when connecting across the Atlantic.&lt;/p&gt;

&lt;p&gt;Despite these results, so far we’ve not been able to switch Meadowrun to use Clear Linux—first it took a long time before we figured out &lt;a href="https://github.com/clearlinux/distribution/issues/2667"&gt;how to configure Clear so that it picks up the AWS user-data&lt;/a&gt;. At the moment &lt;a href="https://community.clearlinux.org/t/nvidia-drivers-on-aws-clear-linux-key-was-rejected-by-service/7709"&gt;we’re struggling with installing Nvidia drivers&lt;/a&gt; on it, which is important for machine learning workloads.&lt;/p&gt;

&lt;p&gt;We’ll see what happens with this one, but if Nvidia drivers are not a blocker and startup times are important to you, do consider using Clear Linux as a base AMI for your EC2 machines.&lt;/p&gt;

&lt;h4&gt;
  
  
  EBS volumes are sluggish at start-up
&lt;/h4&gt;

&lt;p&gt;The next issue we looked at is Python startup times. To run a function on the EC2 machine, meadowrun starts a Python process which reads the function to run and its pickled arguments from disk, and runs it. However, just starting that Python process without actually running anything, on first startup took about 7–8 seconds.&lt;/p&gt;

&lt;p&gt;This seems extreme even for Python (yes, we still cracked the inevitable “we should rewrite it in Rust” jokes).&lt;/p&gt;

&lt;p&gt;A run with &lt;code&gt;-X importtime&lt;/code&gt; revealed the worst offenders, but unfortunately it’s mostly boto3 which we need to talk to the AWS API, for example to pull Docker images.&lt;/p&gt;

&lt;p&gt;Also, subsequent process startups on the same machine take a more reasonable 1–2 seconds. So what’s going on? Our first idea was file system caching, which was a close but ultimately wrong guess.&lt;/p&gt;

&lt;p&gt;It turned out that AWS’ EBS (Elastic Block Store) which provides the actual storage for EC2 machines we were measuring, &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-initialize.html"&gt;has a warmup time&lt;/a&gt;. AWS recommends reading the entire device to warm up the EBS volume by using &lt;code&gt;dd&lt;/code&gt;. That works in that after the warmup, Python’s startup time drops to 1–2 seconds. Unfortunately reading even a 16GB volume takes over 100 seconds. Reading just the files in the meadowrun environment instead still takes about 20 seconds. So this solution won’t work for us—the warmup time takes longer than the cold start.&lt;/p&gt;

&lt;p&gt;There is a possibility to pay for “hot” EBS snapshots which give warmed-up performance from the get-go, but it’s crazy expensive because &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-fast-snapshot-restore.html"&gt;you’re billed per hour, per availability zone, per snapshot as long as fast snapshot restore is enabled&lt;/a&gt;. This comes to $540/month for a single snapshot for a single availability zone, and a single region like us-east-1 has 6 availability zones!&lt;/p&gt;

&lt;p&gt;At the end of the day, looks like we’ll just have to suck this one up. If you’re an enterprise that’s richer than god, consider enabling hot EBS snapshots. For some use cases reading the entire disk up front is a more appropriate and certainly much cheaper alternative.&lt;/p&gt;

&lt;h4&gt;
  
  
  If your code is async, use async libraries
&lt;/h4&gt;

&lt;p&gt;From the start, meadowrun was using &lt;a href="https://www.fabfile.org/"&gt;fabric&lt;/a&gt; to execute SSH commands on EC2 machines. Fabric isn’t natively async though, while meadowrun is—meadowrun manages almost exclusively I/O bound operations, such as calling AWS APIs and SSH’ing into remote machines, so async makes sense for us.&lt;/p&gt;

&lt;p&gt;The friction with Fabric caused some headaches, like having to start a thread to make Fabric appear async with respect to the rest of the code. This was not only ugly, but also slow: when executing a parallel distributed computation using Meadowrun’s &lt;code&gt;run_map&lt;/code&gt;, meadowrun was spending a significant amount of time waiting for Fabric.&lt;/p&gt;

&lt;p&gt;We have now &lt;a href="https://github.com/meadowdata/meadowrun/pull/101"&gt;fully switched&lt;/a&gt; to a natively async SSH client, &lt;a href="https://asyncssh.readthedocs.io/en/latest/index.html"&gt;asyncssh&lt;/a&gt;. Performance for &lt;code&gt;run_map&lt;/code&gt; has improved considerably: with fabric, for a map with 20 concurrent tasks, median execution time over 10 runs was about 110 seconds. With asyncssh, this dropped to 15 seconds—a 7x speedup.&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;Not everything you find while optimizing is actionable, and that’s ok—at the very least you’ll have learnt something!&lt;/p&gt;

&lt;p&gt;Takeaways which you may be able to apply in your own adventures with AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If startup time is important, try using Clear Linux.&lt;/li&gt;
&lt;li&gt;EBS volumes need some warmup to reach full I/O speed—you can avoid this if you have lots of money to spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;To stay updated on Meadowrun, star us on &lt;a href="https://github.com/meadowdata/meadowrun"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001"&gt;Twitter&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ec2</category>
      <category>performance</category>
      <category>python</category>
      <category>aws</category>
    </item>
    <item>
      <title>Use AWS to unzip all of Wikipedia in 10 minutes</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Wed, 29 Jun 2022 19:20:00 +0000</pubDate>
      <link>https://dev.to/meadowrun/use-aws-to-unzip-all-of-wikipedia-in-10-minutes-32h5</link>
      <guid>https://dev.to/meadowrun/use-aws-to-unzip-all-of-wikipedia-in-10-minutes-32h5</guid>
      <description>&lt;p&gt;This is the first article in a series that walks through how to use &lt;a href="https://meadowrun.io/"&gt;Meadowrun&lt;/a&gt; to quickly run regular expressions over a large text dataset. This first article reviews parsing the Wikipedia dump file format, and then walks through using Meadowrun and EC2 to unzip ~67GB (uncompressed) of articles from the English language Wikipedia dump file. The &lt;a href="https://dev.to/meadowrun/using-aws-and-hyperscan-to-match-regular-expressions-on-100gb-of-text-50fe"&gt;second article&lt;/a&gt; will cover running regular expressions over the extracted data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background and Motivation
&lt;/h2&gt;

&lt;p&gt;The goal of this first post is mostly to walk through creating a large text dataset (the contents of English-language Wikipedia) so that we have a real dataset to work with for the second article in this series. This article also introduces Meadowrun as a tool that makes it easy to scale your python code into the cloud.&lt;/p&gt;

&lt;p&gt;If you want to understand some of the details of the Wikipedia dataset, start with this article. If you’re interested in generally applicable examples of searching large text datasets very quickly, start with the &lt;a href="https://dev.to/meadowrun/using-aws-and-hyperscan-to-match-regular-expressions-on-100gb-of-text-50fe"&gt;second article&lt;/a&gt;, and come back if you want to be able to follow along using the same dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unzipping Wikipedia
&lt;/h2&gt;

&lt;p&gt;Wikipedia (as explained &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Database_download#English-language_Wikipedia"&gt;here&lt;/a&gt;) provides a &lt;a href="https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles-multistream.xml.bz2"&gt;“multistream” XML dump&lt;/a&gt; (caution! that’s a link to a ~19GB file). This is a single file that is effectively multiple bz2 files concatenated together. It’s meant to be read using the &lt;a href="https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles-multistream-index.txt.bz2"&gt;“index” file&lt;/a&gt;, which is a single bz2 text file whose contents look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;602:594:Apollo
602:595:Andre Agassi
683215:596:Artificial languages
683215:597:Austroasiatic languages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is saying that there’s an article called “Apollo” with article ID 594, which is in the section of the file starting at 602 bytes into the multistream dump. The next article is “Andre Agassi”, has article ID 595, and it’s in the same section that starts at 602 bytes (each section has 100 articles). The article after that is called “Artificial languages”, has article ID 596, and it’s in the next section which starts at 683215 bytes.&lt;/p&gt;

&lt;p&gt;So we’ll write a function &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0#file-unzip_wikipedia_articles-py"&gt;iterate_articles_chunk&lt;/a&gt; that takes a &lt;code&gt;multistream_file&lt;/code&gt;, a &lt;code&gt;index_file&lt;/code&gt;, skips the first &lt;code&gt;article_offset&lt;/code&gt; articles, and then read &lt;code&gt;num_articles&lt;/code&gt;. A few notes on this code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We’re using &lt;a href="https://github.com/RaRe-Technologies/smart_open"&gt;smart_open&lt;/a&gt;, which is an amazing library that lets you open objects in S3 (and other cloud object stores) as if they’re files on your filesystem. It’s obviously critical that we’re able to seek to an arbitrary position in an S3 file without first downloading the whole thing. We’ll assume you’re using &lt;a href="https://python-poetry.org/"&gt;Poetry&lt;/a&gt;, but you should be able to follow along with any other package manager:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poetry add smart_open[s3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  We’re ignoring a ton of metadata about each Wikipedia article, but that doesn’t matter for our purposes.&lt;/li&gt;
&lt;li&gt;  A bit of an aside, but the above code is also a good example of how to use &lt;a href="https://docs.python.org/3/library/xml.etree.elementtree.html"&gt;xml.etree.ElementTree.XMLPullParser&lt;/a&gt; to parse an XML file as a stream, which makes sense for large files, as it means you don’t need to hold the entire file in memory. In contrast, &lt;a href="https://docs.python.org/3/library/xml.dom.minidom.html"&gt;xml.dom.minidom&lt;/a&gt; requires enough memory for your entire file, but it does allow processing elements in any order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s try it out!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unzip_wikipedia_articles&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;iterate_articles_chunk&lt;/span&gt;

&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iterate_articles_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"enwiki-latest-pages-articles-multistream-index.txt.bz2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"enwiki-latest-pages-articles-multistream.xml.bz2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"Read ~&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bytes_read&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; articles in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read ~34,795,421 bytes from 1000 articles in 2.17s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Man that’s slow! &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0#file-count_articles-py"&gt;Counting the lines in the index file&lt;/a&gt; tells us there are 22,114,834 articles (this is as of the 2022–06–20 dump). So at 2.17s per 1000 articles times 22 million articles, I’m looking at around 13 hours to unzip this entire file on my i7-8550U processor. Presumably most of this time is decompressing bz2, so as a sanity check, let’s see what others are getting for bz2 decompression speeds. &lt;a href="https://www.rootusers.com/gzip-vs-bzip2-vs-xz-performance-comparison/"&gt;This article&lt;/a&gt; gives 24MB/s for decompression speed, and we’re in the same ballpark at 16MB/s (we’re not counting the bytes for XML tags we’re ignoring, so our true decompression speed is a bit faster than this).&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling with Meadowrun
&lt;/h2&gt;

&lt;p&gt;We could use multiprocessing to make use of all the cores on my laptop, but that would still only get us to 1–2 hours of runtime at best. In order to get through this in a more reasonable amount of time, we’ll need to use multiple machines in EC2. Meadowrun makes this easy!&lt;/p&gt;

&lt;p&gt;We’ll assume you’ve configured your AWS CLI, and we’ll continue using Poetry. (&lt;a href="https://docs.meadowrun.io/en/stable/"&gt;See the docs&lt;/a&gt; for more context, as well as for using Meadowrun with Azure, pip, or conda.) To get started, install the Meadowrun package and then run Meadowrun’s &lt;code&gt;install&lt;/code&gt; command to set up the resources Meadowrun needs in your AWS account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poetry add meadowrun
poetry run meadowrun-manage-ec2 install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll need to create an S3 bucket, upload the data files there, and then give the Meadowrun IAM role access to that bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;wikipedia&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="n"&gt;enwiki&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;multistream&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bz2&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;wikipedia&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="n"&gt;enwiki&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;multistream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bz2&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;wikipedia&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;

&lt;span class="n"&gt;poetry&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;manage&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="n"&gt;grant&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="n"&gt;wikipedia&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we’re ready to run our unzipping on the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;meadowrun&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;convert_to_tar&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;convert_articles_chunk_to_tar_gz&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unzip_all_articles&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;total_articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22_114_834&lt;/span&gt;
    &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;convert_articles_chunk_to_tar_gz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_articles&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EC2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mirror_local&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;num_concurrent_tasks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unzip_all_articles&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this snippet, we’re splitting all of the articles into chunks of 100,000, which gives us 222 tasks. We’re telling Meadowrun to start up enough EC2 instances to run 64 of these tasks in parallel at a time, and that each task will need 1 CPU and 2 GB of RAM. And we’re okay with spot instances up to an 80% chance of eviction (aka interruption).&lt;/p&gt;

&lt;p&gt;Each task will run &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0#file-convert_to_tar-py"&gt;convert_articles_chunk_to_tar_gz&lt;/a&gt; which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calls &lt;code&gt;iterate_articles_chunk&lt;/code&gt; to read its chunk of 100,000 articles&lt;/li&gt;
&lt;li&gt;Gets just the title and text of those articles&lt;/li&gt;
&lt;li&gt;Packs those into a .tar.gz file of plain text files where the name of each file in the archive is the title of the article (a .gz file is much faster to decompress than a bz2 file)&lt;/li&gt;
&lt;li&gt;And finally writes that new file back to S3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Meadowrun and EC2, this takes about 10 minutes from start to finish, where this would have taken 13 hours on my laptop just for reading the articles, not even counting the time to recompress into a .tar.gz.&lt;/p&gt;

&lt;p&gt;The exact instance type that Meadowrun selects will vary based on spot instance availability and real-time pricing, but in an example run Meadowrun prints out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launched 1 new instance(s) (total $0.9107/hr) for the remaining 64 workers:
    ec2-3-12-160-131.us-east-2.compute.amazonaws.com: r6i.16xlarge (64 CPU/512.0 GB), spot ($0.9107/hr, 2.5% chance of interruption), will run 64 job/worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At $0.9107/hr, this whole process costs us less than a quarter!&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing remarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  EC2 is amazing! (And so are Azure and GCP.) Spot pricing makes really powerful machines accessible for not very much money.&lt;/li&gt;
&lt;li&gt;  On the other hand, using EC2 for a task like this can require a decent amount of setup in terms of selecting an instance, remembering to turn it off when you’re done, and getting your code and libraries onto the machine. Meadowrun makes all of that easy!&lt;/li&gt;
&lt;li&gt;  The complete code for this series is &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0"&gt;here&lt;/a&gt; in case you want to use it as a template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;To stay updated on Meadowrun, star us on &lt;a href="https://github.com/meadowdata/meadowrun"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001"&gt;Twitter&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>aws</category>
    </item>
    <item>
      <title>Using AWS and Hyperscan to match regular expressions on 100GB of text</title>
      <dc:creator>Hyunho Richard Lee</dc:creator>
      <pubDate>Wed, 29 Jun 2022 19:10:00 +0000</pubDate>
      <link>https://dev.to/meadowrun/using-aws-and-hyperscan-to-match-regular-expressions-on-100gb-of-text-50fe</link>
      <guid>https://dev.to/meadowrun/using-aws-and-hyperscan-to-match-regular-expressions-on-100gb-of-text-50fe</guid>
      <description>&lt;p&gt;This is the second article in a series that walks through how to use &lt;a href="https://meadowrun.io/"&gt;Meadowrun&lt;/a&gt; to quickly run regular expressions over a large text dataset, using English-language Wikipedia as our example data set. If you want to follow along with this second article, you’ll need the simplified article extracts that we produce in the &lt;a href="https://dev.to/meadowrun/use-aws-to-unzip-all-of-wikipedia-in-10-minutes-32h5"&gt;first article&lt;/a&gt;. Alternatively, it should be pretty simple to translate the code in this post to work with any data set in any data format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background and motivation
&lt;/h2&gt;

&lt;p&gt;This series is inspired by past projects at hedge funds for parsing credit card transaction data. To oversimplify quite a bit, we would look at the description fields of each transaction to see if it matched a tradeable public company, add up the amounts for all the transactions for each company, and use that to try to predict revenue for each company.&lt;/p&gt;

&lt;p&gt;There were a lot of challenges to making these revenue forecasts accurate. The problem we’ll focus on this article is that for some reason (which &lt;a href="https://bam.kalzumeus.com/archive/"&gt;patio11&lt;/a&gt; could probably explain in depth), the description field for credit card transactions would come to us completely garbled. For a company like McDonald’s, we would see variations like &lt;code&gt;mcdonalds&lt;/code&gt;, &lt;code&gt;mcdonald's&lt;/code&gt;, &lt;code&gt;mcdonalds&lt;/code&gt;, &lt;code&gt;mcdonald s&lt;/code&gt;, &lt;code&gt;mcd&lt;/code&gt;, and even misspellings and typos like &lt;code&gt;mcdnalds&lt;/code&gt;. Our solution was to create regular expressions that covered all of the common variations of all of the brands of each company we were interested in.&lt;/p&gt;

&lt;p&gt;This dataset was about a terabyte uncompressed, and we had hundreds of regular expressions, which meant that we needed two main pieces of infrastructure: a really fast regular expression library and a distributed computation engine. For regular expressions we used Hyperscan which we’ll introduce here. Our distributed computation engine isn’t publicly available, but we’ll introduce Meadowrun which works in a similar way.&lt;/p&gt;

&lt;p&gt;The credit card dataset we used obviously isn’t available publicly as well, so we’ll use English-language Wikipedia as a stand-in (~67 GB uncompressed), as the goal of this article is to walk through the engineering aspects of this analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting up to speed
&lt;/h2&gt;

&lt;p&gt;If you didn’t follow along with the &lt;a href="https://dev.to/meadowrun/use-aws-to-unzip-all-of-wikipedia-in-10-minutes-32h5"&gt;first article&lt;/a&gt; in this series, you should be able to follow this article with your own dataset as long as you install &lt;a href="https://github.com/RaRe-Technologies/smart_open"&gt;smart_open&lt;/a&gt; and Meadowrun. smart_open is an amazing library that lets you open objects in S3 (and other cloud object stores) as if they’re files on your filesystem, and Meadowrun makes it easy to run your Python code on the cloud.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poetry add smart_open[s3]
poetry add meadowrun
poetry run meadowrun-manage-ec2 install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll assume you’re using Poetry here, but feel free to use any environment manager. We’ll also assume you’ve configured your AWS CLI. (&lt;a href="https://docs.meadowrun.io/en/stable/"&gt;See the docs&lt;/a&gt; for more context, as well as for using Meadowrun with Azure, pip, and/or conda.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Some realistic data
&lt;/h2&gt;

&lt;p&gt;We’ll use simplified extracts of English-language Wikipedia as generated by the code in the first article in this series. As a quick overview, that produces 222 files like s3://wikipedia-meadowrun-demo/extracted-200000.tar.gz, which is a tar.gz file containing the 200,000th through 299,999th Wikipedia article. The filenames are the titles of the articles and the contents of each file are the text of the corresponding article. We’ll need a little function that can read one of these tar files, &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0#file-read_articles_extract-py"&gt;iterate_extract&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For our regular expressions, we’ll do a simplified version of what I describe above and just take the names of the companies in the S&amp;amp;P500, which we get, of course, from &lt;a href="https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"&gt;Wikipedia&lt;/a&gt;. (If you’re curious, this is the 1379418th article in the 2022–06–20 index, so you can also find it in s3://wikipedia-meadowrun-demo/extracted-1300000.tar.gz.) I used a little bit of elbow grease to get this into a file called &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0?file=companies.txt"&gt;companies.txt&lt;/a&gt;, and the first few lines look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3M
A. O. Smith
Abbott
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll upload this to S3 with &lt;code&gt;aws s3 cp companies.txt s3://wikipedia-meadowrun-demo/companies.txt&lt;/code&gt;, and use this bit of code to turn this into a simple regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;smart_open&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;company_names_regex&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;smart_open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"s3://wikipedia-meadowrun-demo/companies.txt"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;companies_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;companies_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"|"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;companies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give us a regex like &lt;code&gt;3M|A.O. Smith|Abbott|...&lt;/code&gt; which lets us look for any occurrence of these company names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regex engines: re vs re2 vs Hyperscan
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems. (&lt;a href="http://regex.info/blog/2006-09-15/247"&gt;Jamie Zawinski&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you haven’t spent too much time in the world of regular expressions, you’ll probably start with the &lt;a href="https://docs.python.org/3/library/re.html"&gt;re standard library&lt;/a&gt;—Python comes with batteries included, after all. Let’s see how this does. We’ll use Meadowrun’s &lt;code&gt;run_function&lt;/code&gt; to run some exploratory code directly on EC2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;re&lt;/span&gt;

&lt;span class="c1"&gt;# import re2 as re
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;meadowrun&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;company_names&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;company_names_regex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;read_articles_extract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;iterate_extract&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_re&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract_file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;compiled_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_names_regex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;article_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article_content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;islice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;iterate_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract_file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;compiled_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="c1"&gt;# print out a little context around a sample of matches
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;article_title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: "&lt;/span&gt;
                    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time_taken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"Scanned &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bytes_scanned&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time_taken&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seconds "&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time_taken&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; B/s)"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_re_ec2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scan_re&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s3://wikipedia-meadowrun-demo/extracted-200000.tar.gz"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EC2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mirror_local&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan_re_ec2&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scan_re&lt;/code&gt; will just extract the first 100 articles from the specified .tar.gz file and run our company name regex on those articles, and tell us how many bytes per second it’s able to process.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scan_re_ec2&lt;/code&gt; uses Meadowrun to run &lt;code&gt;scan_re&lt;/code&gt; on an EC2 instance — here we’re asking it to start an EC2 instance that has at least 1 CPU and 2 GB of memory, and that we’re okay with spot instances up to an 80% chance of eviction (aka interruption). We could just run &lt;code&gt;scan_re&lt;/code&gt; locally, but this will actually be faster overall because downloading data from S3 is significantly faster from EC2 than over the internet. In other words, we’re sending our code to our data rather than the other way around.&lt;/p&gt;

&lt;p&gt;Running this gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scanned 1,781,196 bytes in 10.38 seconds (171,554 B/s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So for this regular expression we’re getting about 170 KB/s. We have about 67 GB, so distributing this over many EC2 instances is still going to be slow and expensive.&lt;/p&gt;

&lt;p&gt;Let’s try &lt;a href="https://github.com/google/re2"&gt;re2&lt;/a&gt;, which is a regular expression engine built by Google &lt;a href="https://github.com/google/re2/wiki/WhyRE2"&gt;primarily with the goal&lt;/a&gt; of taking linear time to search a string for any regular expression. For context, python’s built-in re library uses a backtracking approach, which can take &lt;a href="https://www.regular-expressions.info/catastrophic.html"&gt;exponential time to search a string&lt;/a&gt;. re2 uses a &lt;a href="https://swtch.com/~rsc/regexp/regexp1.html"&gt;Thompson NFA approach&lt;/a&gt;, which can guarantee the linear time search, but offers fewer features.&lt;/p&gt;

&lt;p&gt;The official python package on PyPI is google-re2, but pyre2 has nicely pre-compiled wheels for Windows and Mac as well (in addition to Linux):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poetry add pyre2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pyre2 is designed as a drop-in replacement, so we can just replace &lt;code&gt;import re&lt;/code&gt; with &lt;code&gt;import re2 as re&lt;/code&gt; in our last snippet and rerun to see what re2’s performance is like. Note that Meadowrun is automatically syncing the changes to our code and environment on the remote machine, and we don’t have to worry about manually rebuilding a virtual environment or a container image with our new re2 library in it.&lt;/p&gt;

&lt;p&gt;Rerunning with re2 and 10,000 articles, we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scanned 190,766,485 bytes in 6.94 seconds (27,479,020 B/s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A massive speedup to 27 MB/s!&lt;/p&gt;

&lt;p&gt;Our last stop is &lt;a href="https://github.com/intel/hyperscan"&gt;Hyperscan&lt;/a&gt;, which is a regular expression engine originally built with an eye towards deep packet inspection by a startup called Sensory Networks which was acquired by Intel in 2013. Hyperscan has a ton of really cool parts to it—there’s a &lt;a href="https://branchfree.org/2019/02/28/paper-hyperscan-a-fast-multi-pattern-regex-matcher-for-modern-cpus/"&gt;good overview&lt;/a&gt; by a maintainer &lt;a href="https://twitter.com/geofflangdale"&gt;Geoff Langdale&lt;/a&gt;, and &lt;a href="https://www.usenix.org/system/files/nsdi19-wang-xiang.pdf"&gt;the paper&lt;/a&gt; goes into more depth. I’ll just highlight one of my favorites, which is its extensive use of &lt;a href="https://branchfree.org/2018/05/30/smh-the-swiss-army-chainsaw-of-shuffle-based-matching-sequences/"&gt;SIMD instructions for searching strings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There’s a compiled version of Hyperscan with python bindings in pypi thanks to &lt;a href="https://python-hyperscan.readthedocs.io/en/latest/"&gt;python-hyperscan&lt;/a&gt;. python-hyperscan only has pre-built wheels for Linux, but that’s fine, as the EC2 instances Meadowrun creates for us are running Linux. We can even tell Poetry to just install Hyperscan if we’re on Linux, as installing it on Windows or Mac will probably fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poetry add hyperscan --platform linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Pip requirements.txt files also support “&lt;a href="https://stackoverflow.com/questions/16011379/operating-system-specific-requirements-with-pip"&gt;environment markers&lt;/a&gt;” which allow you to accomplish the same thing.)&lt;/p&gt;

&lt;p&gt;Hyperscan’s API isn’t a drop-in replacement for re2, so we’ll need to adjust our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;meadowrun&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;company_names&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;company_names_regex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;read_articles_extract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;iterate_extract&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_hyperscan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;take_first_n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;print_matches&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hyperscan&lt;/span&gt;

    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;print_matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;article_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
            &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;article_title&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;from_index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;to_index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hyperscan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;# expression, id, flags
&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;company_names_regex&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;hyperscan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HS_FLAG_CASELESS&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;hyperscan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HS_FLAG_SOM_LEFTMOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expressions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expressions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expressions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;article_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article_content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;islice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;iterate_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract_file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;take_first_n&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;match_event_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;time_taken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"Scanned &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bytes_scanned&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time_taken&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seconds "&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bytes_scanned&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time_taken&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; B/s)"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_hyperscan_ec2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scan_hyperscan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"s3://wikipedia-meadowrun-demo/extracted-200000.tar.gz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EC2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mirror_local&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan_hyperscan_ec2&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scan_hyperscan&lt;/code&gt; shows a really basic usage of the python-hyperscan API, and &lt;code&gt;scan_hyperscan_ec2&lt;/code&gt; is doing the same thing of using Meadowrun to run this on EC2. Running this gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scanned 190,766,485 bytes in 2.74 seconds (69,679,969 B/s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is another solid improvement on top of re2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Now we can use Meadowrun’s &lt;code&gt;run_map&lt;/code&gt; to run this over all of Wikipedia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;meadowrun&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;scan_wikipedia_hyperscan&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;scan_hyperscan&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_hyperscan_ec2_full&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="n"&gt;total_articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22_114_834&lt;/span&gt;
    &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scan_hyperscan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"s3://wikipedia-meadowrun-demo/extracted-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.tar.gz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_articles&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_articles&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocCloudInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EC2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;logical_cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_gb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_eviction_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;meadowrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mirror_local&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;num_concurrent_tasks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan_hyperscan_ec2_full&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run_map&lt;/code&gt; has similar semantics to python’s built-in &lt;a href="https://docs.python.org/3/library/functions.html#map"&gt;map&lt;/a&gt; function. If you haven’t seen it before, &lt;code&gt;map(f, xs)&lt;/code&gt; is roughly equivalent to &lt;code&gt;[f(x) for x in xs]&lt;/code&gt;. &lt;code&gt;run_map&lt;/code&gt; is doing the same thing as &lt;code&gt;map&lt;/code&gt; but in parallel on the cloud. So we’re requesting the same CPU/memory per task as before, and we’re asking Meadowrun to start enough EC2 instances that we can run 64 tasks in parallel. Each task will run &lt;code&gt;scan_hyperscan&lt;/code&gt; on a different extract file, although we’re actually going over the dataset twice to synthetically make the dataset a bit larger.&lt;/p&gt;

&lt;p&gt;The exact instance type that Meadowrun selects will vary based on spot instance availability and real-time pricing, but in an example run Meadowrun prints out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launched 1 new instance(s) (total $0.6221/hr) for the remaining 64 workers:
    ec2-18-117-89-205.us-east-2.compute.amazonaws.com: c5a.16xlarge (64 CPU/128.0 GB), spot ($0.6221/hr, 2.5% chance of interruption), will run 64 job/worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The doubled Wikipedia data set is about 135GB and searching for any occurrence of a company name in the S&amp;amp;P 500 takes about 5 minutes and only costs us about 5 cents!&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing remarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Hyperscan can be a bit annoying to use, as it has a different API from re and there aren’t any precompiled versions for Windows or Mac (it is possible to compile for these platforms, it just requires a bit of work). In these very unscientific benchmarks it’s “only” 2.5x faster than re2, but in my experience it’s worth it because as your regular expressions get larger and more complicated, the gap in performance compared to re2 gets larger. And it’s really quite a marvel of both computer science theory and engineering.&lt;/li&gt;
&lt;li&gt;  Meadowrun makes it easy to use really powerful machines in EC2 (or Azure) to process your data. Obviously for this exact workload it would be faster to use Google or Wikipedia’s own search functionality, but the approach we’re showing here can be used on any large text dataset with arbitrarily complicated regular expressions. For anything that isn’t already indexed by Google or another tech giant, I don’t think there’s a better combination of tools.&lt;/li&gt;
&lt;li&gt;  The complete code for this series is &lt;a href="https://gist.github.com/hrichardlee/4be2881a66faaee24f122eeaccf0b2c0"&gt;here&lt;/a&gt; in case you want to use it as a template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;To stay updated on Meadowrun, star us on &lt;a href="https://github.com/meadowdata/meadowrun"&gt;Github&lt;/a&gt; or follow us on &lt;a href="https://twitter.com/kurt2001"&gt;Twitter&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
