{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Initialization boilerplate\n", "from typing import *\n", "\n", "import os\n", "import ibm_watson\n", "import ibm_watson.natural_language_understanding_v1 as nlu\n", "import ibm_cloud_sdk_core\n", "import pandas as pd\n", "import text_extensions_for_pandas as tp\n", "import urllib\n", "\n", "import ray\n", "import spacy\n", "import multiprocessing\n", "import time\n", "import threading\n", "import matplotlib.pyplot as plt\n", "\n", "# Remove silly SpaCy warnings about not having a GPU\n", "def fix_spacy_warnings():\n", " import warnings\n", " warnings.filterwarnings(action='ignore', \\\n", " category=UserWarning, message='.*User provided device_type.*')\n", "fix_spacy_warnings()\n", "\n", "# Remove silly Huggingface warnings about not using a useless parallel tokenizer.\n", "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n", "\n", "api_key = os.environ.get(\"IBM_API_KEY\")\n", "service_url = os.environ.get(\"IBM_SERVICE_URL\") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Part 5: Scale up some more with Pandas and Ray\n", "\n", "Let's start by summarizing our progress so far.\n", "\n", "In [Part 1](./Market_Intelligence_Part1.ipynb), we defined an NLP pipeline that used Watson Natural Language Understanding and Text Extensions for Pandas to identify the names of executives in corporate press releases.\n", "\n", "In [Part 2](./Market_Intelligence_Part2.ipynb), we extended our NLP pipeline by using SpaCy and Text Extensions for Pandas to associate each executive name with a job title.\n", "\n", "If we separate the expensive NLP model evaluations from the rest of our code, we can think of this processing pipeline as having five distinct stages, as shown in the following diagram.\n", "\n", "![First version of our processing pipeline](images/pipeline_v1.png)\n", "\n", "In [Part 3](./Market_Intelligence_Part3.ipynb), we applied the semijoin trick with Text Extensions for Pandas to reduce the cost of running the SpaCy dependency parser. This change decreased the cost of parsing by a factor of 9 and improved end-to-end running time by a factor of 3.\n", "\n", "In [Part 4](./Market_Intelligence_Part4.ipynb), we showed how you can use [Ray](ray.io) to rate-limit requests to a remote web service such as Watson Natural Language Understanding.\n", "\n", "In this final part of the tutorial, we use Ray to take the performance improvements from Part 3 to the next level by applying parallel processing. By the time we're done, we'll have a version of our pipeline that runs **300 times faster** than the pipeline we started with at the beginning of Part 3.\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
urlhtml
0https://newsroom.ibm.com/2020-02-04-The-Avril-...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
1https://newsroom.ibm.com/2020-02-11-IBM-X-Forc...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
2https://newsroom.ibm.com/2020-02-18-IBM-Study-...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
3https://newsroom.ibm.com/2020-02-19-IBM-Power-...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
4https://newsroom.ibm.com/2020-02-20-Centotrent...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
.........
186https://newsroom.ibm.com/2021-01-25-OVHcloud-t...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
187https://newsroom.ibm.com/2021-01-26-Luminor-Ba...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
188https://newsroom.ibm.com/2021-01-26-DIA-Levera...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
189https://newsroom.ibm.com/2021-01-26-IBM-Board-...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
190https://newsroom.ibm.com/2021-01-26-Latin-Amer...<!DOCTYPE html public \"-//W3C//DTD HTML 4.01 T...
\n", "

191 rows × 2 columns

\n", "
" ], "text/plain": [ " url \\\n", "0 https://newsroom.ibm.com/2020-02-04-The-Avril-... \n", "1 https://newsroom.ibm.com/2020-02-11-IBM-X-Forc... \n", "2 https://newsroom.ibm.com/2020-02-18-IBM-Study-... \n", "3 https://newsroom.ibm.com/2020-02-19-IBM-Power-... \n", "4 https://newsroom.ibm.com/2020-02-20-Centotrent... \n", ".. ... \n", "186 https://newsroom.ibm.com/2021-01-25-OVHcloud-t... \n", "187 https://newsroom.ibm.com/2021-01-26-Luminor-Ba... \n", "188 https://newsroom.ibm.com/2021-01-26-DIA-Levera... \n", "189 https://newsroom.ibm.com/2021-01-26-IBM-Board-... \n", "190 https://newsroom.ibm.com/2021-01-26-Latin-Amer... \n", "\n", " html \n", "0 \n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
0[1281, 1292): 'Rob DiCicco'[1294, 1348): 'PharmD, Deputy Chief Health Off...
0[1213, 1229): 'Christoph Herman'[1231, 1281): 'SVP and Head of SAP HANA Enterp...
1[2227, 2242): 'Stephen Leonard'[2244, 2282): 'General Manager, IBM Cognitive ...
0[2290, 2298): 'Bob Lord'[2300, 2376): 'IBM Senior Vice President of Co...
.........
0[3113, 3123): 'Mike Doran'[3125, 3156): 'Worldwide Sales Director at IBM'
0[3156, 3170): 'Howard Boville'[3172, 3211): 'Senior Vice President, IBM Hybr...
0[3116, 3139): 'Samuel Brack Co-Founder'[3129, 3154): 'Co-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
\n", "

260 rows × 2 columns

\n", "" ], "text/plain": [ " person \\\n", "0 [1977, 1991): 'Wendi Whitmore' \n", "0 [1281, 1292): 'Rob DiCicco' \n", "0 [1213, 1229): 'Christoph Herman' \n", "1 [2227, 2242): 'Stephen Leonard' \n", "0 [2290, 2298): 'Bob Lord' \n", ".. ... \n", "0 [3113, 3123): 'Mike Doran' \n", "0 [3156, 3170): 'Howard Boville' \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [1488, 1498): 'Ana Zamper' \n", "\n", " title \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... \n", "0 [1294, 1348): 'PharmD, Deputy Chief Health Off... \n", "0 [1231, 1281): 'SVP and Head of SAP HANA Enterp... \n", "1 [2244, 2282): 'General Manager, IBM Cognitive ... \n", "0 [2300, 2376): 'IBM Senior Vice President of Co... \n", ".. ... \n", "0 [3125, 3156): 'Worldwide Sales Director at IBM' \n", "0 [3172, 3211): 'Senior Vice President, IBM Hybr... \n", "0 [3129, 3154): 'Co-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "spacy_language_model = spacy.load(\"en_core_web_trf\")\n", "nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", "nlu_api.set_service_url(service_url)\n", "\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " # Steps 1 and 2, as implemented in find_persons_quoted_by_name()\n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " \n", " # Steps 3 and 4, as implemented in find_titles_of_persons()\n", " step_3_results = mi.perform_dependency_parsing(step_1_results[\"analyzed_text\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n", "\n", "# Repeat steps 1-4 on every document\n", "dataframes_to_stack = [\n", " steps_1_through_4(doc_html) for doc_html in articles[\"html\"]\n", "]\n", "\n", "# Step 5: Merge the results across documents\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The exact running time for the above cell varies depending on which machine you use to run the notebook and what other processes are running in the background. On a 2020 Macbook Pro, it takes around 850 seconds. Let's see if we can improve on that running time." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Recap: Accelerating the baseline pipeline with the semijoin trick" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In Part 3, we showed how to improve the end-to-end performance of this pipeline by applying the semijoin trick to the expensive dependency parsing step. This change made parsing 9x faster, improving the end-to-end performance by a factor of 3. Here's what our five-stage pipeline looks like after that change." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 1min 52s, sys: 3.12 s, total: 1min 55s\n", "Wall time: 4min 23s\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
0[1281, 1292): 'Rob DiCicco'[1294, 1348): 'PharmD, Deputy Chief Health Off...
0[1213, 1229): 'Christoph Herman'[1231, 1281): 'SVP and Head of SAP HANA Enterp...
1[2227, 2242): 'Stephen Leonard'[2244, 2282): 'General Manager, IBM Cognitive ...
0[2290, 2298): 'Bob Lord'[2300, 2376): 'IBM Senior Vice President of Co...
.........
0[3113, 3123): 'Mike Doran'[3125, 3156): 'Worldwide Sales Director at IBM'
0[3156, 3170): 'Howard Boville'[3172, 3211): 'Senior Vice President, IBM Hybr...
0[3116, 3139): 'Samuel Brack Co-Founder'[3131, 3154): '-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
\n", "

260 rows × 2 columns

\n", "
" ], "text/plain": [ " person \\\n", "0 [1977, 1991): 'Wendi Whitmore' \n", "0 [1281, 1292): 'Rob DiCicco' \n", "0 [1213, 1229): 'Christoph Herman' \n", "1 [2227, 2242): 'Stephen Leonard' \n", "0 [2290, 2298): 'Bob Lord' \n", ".. ... \n", "0 [3113, 3123): 'Mike Doran' \n", "0 [3156, 3170): 'Howard Boville' \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [1488, 1498): 'Ana Zamper' \n", "\n", " title \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... \n", "0 [1294, 1348): 'PharmD, Deputy Chief Health Off... \n", "0 [1231, 1281): 'SVP and Head of SAP HANA Enterp... \n", "1 [2244, 2282): 'General Manager, IBM Cognitive ... \n", "0 [2300, 2376): 'IBM Senior Vice President of Co... \n", ".. ... \n", "0 [3125, 3156): 'Worldwide Sales Director at IBM' \n", "0 [3172, 3211): 'Senior Vice President, IBM Hybr... \n", "0 [3131, 3154): '-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "spacy_language_model = spacy.load(\"en_core_web_trf\")\n", "nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", "nlu_api.set_service_url(service_url)\n", "\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " # Steps 1 and 2, as implemented in find_persons_quoted_by_name()\n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " \n", " # Steps 3 and 4, as implemented in find_titles_of_persons()\n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n", "\n", "# Repeat steps 1-4 on every document\n", "dataframes_to_stack = [\n", " steps_1_through_4(doc_html) for doc_html in articles[\"html\"]\n", "]\n", "\n", "# Step 5: Merge the results across documents\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This code runs in about 280 seconds, a 3x improvement from the initial baseline version.\n", "\n", "Now it's time to deploy parallel processing and improve that performance some more." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## First version: Wrap the entire processing pipeline in a `@ray.remote` decorator." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Ray](https://ray.io) is a system for parallel processing that is designed to work at a wide variety of scales. As you tune the accuracy of an NLP application, you'll move between different sized inputs. You may start out by examining results on individual documents on your laptop, then switch to processing dozens of documents on your laptop. Later you may run \n", "through thousands of documents on a server. Eventually, you could deploy to production and process millions of documents on a cluster. And when there's a problem in production you might find yourself back again to working on your laptop for the next round of tuning.\n", "\n", "Ray lets you code up your processing pipeline once and have it work well across this wide variety of scales. That way you can use the same code at every point throughout this iterative process.\n", "\n", "In the remainder of this part of the tutorial, we're going to show how to parallelize our end-to-end pipeline using Ray. Once we have it running in parallel on a laptop, we'll take the same code over to a large server to get even more parallel speedup.\n", "\n", "The first step is to start up a Ray cluster by calling `ray.init()`." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:15:07,175\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "def reboot_ray():\n", " if ray.is_initialized():\n", " ray.shutdown()\n", " ray.init()\n", " \n", "reboot_ray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's try the simplest approach possible: Wrap our document processing code in a Ray remote function. To create a remote function, we just need to define a Python function and add the `@ray.remote` decorator to the function:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "scrolled": true }, "outputs": [], "source": [ "# NOTE: The blog version of this cell should show this code side-by-side with the \n", "# original code and highlight the changes.\n", "\n", "spacy_language_model = spacy.load(\"en_core_web_trf\")\n", "nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", "nlu_api.set_service_url(service_url)\n", "\n", "# @ray.remote decorator defines a Ray task\n", "@ray.remote\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can push the processing of a document to the Ray cluster by spawning a copy of the remote function. To spawn a copy of the function, we call the remote function's `remote()` method, which starts running the function in the background and returns a *future* -- a placeholder for the result that the function will produce when it completes.\n", "\n", "If we pass the future to `ray.get()`, then Ray will block until the function has completed, download the result to the calling process, and return the result:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Could not serialize the function 1686926749.steps_1_through_4. Check https://docs.ray.io/en/master/serialization.html#troubleshooting for more information.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/remote_function.py\u001b[0m in \u001b[0;36m_remote\u001b[0;34m(self, args, kwargs, num_returns, num_cpus, num_gpus, memory, object_store_memory, accelerator_type, resources, max_retries, retry_exceptions, placement_group, placement_group_bundle_index, placement_group_capture_child_tasks, runtime_env, name, scheduling_strategy)\u001b[0m\n\u001b[1;32m 285\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 286\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_pickled_function\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpickle\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_function\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 287\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mTypeError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/cloudpickle/cloudpickle_fast.py\u001b[0m in \u001b[0;36mdumps\u001b[0;34m(obj, protocol, buffer_callback)\u001b[0m\n\u001b[1;32m 72\u001b[0m )\n\u001b[0;32m---> 73\u001b[0;31m \u001b[0mcp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 74\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfile\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetvalue\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/cloudpickle/cloudpickle_fast.py\u001b[0m in \u001b[0;36mdump\u001b[0;34m(self, obj)\u001b[0m\n\u001b[1;32m 619\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 620\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mPickler\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 621\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mRuntimeError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: cannot pickle '_thread.RLock' object", "\nThe above exception was the direct cause of the following exception:\n", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m/var/folders/bd/k5pyhn0130708d7y9q2pjj380000gn/T/ipykernel_68713/418004903.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mdoc_html\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marticles\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0miloc\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"html\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mfuture\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msteps_1_through_4\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremote\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdoc_html\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mstep_4_results\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfuture\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/remote_function.py\u001b[0m in \u001b[0;36m_remote_proxy\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0mwraps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfunction\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 138\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_remote_proxy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 139\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_remote\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 140\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremote\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_remote_proxy\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/util/tracing/tracing_helper.py\u001b[0m in \u001b[0;36m_invocation_remote_span\u001b[0;34m(self, args, kwargs, *_args, **_kwargs)\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mkwargs\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 294\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0;34m\"_ray_trace_ctx\"\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 295\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0m_args\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0m_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 296\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 297\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0;34m\"_ray_trace_ctx\"\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/opt/miniconda3/envs/pd/lib/python3.8/site-packages/ray/remote_function.py\u001b[0m in \u001b[0;36m_remote\u001b[0;34m(self, args, kwargs, num_returns, num_cpus, num_gpus, memory, object_store_memory, accelerator_type, resources, max_retries, retry_exceptions, placement_group, placement_group_bundle_index, placement_group_capture_child_tasks, runtime_env, name, scheduling_strategy)\u001b[0m\n\u001b[1;32m 291\u001b[0m \u001b[0;34m\"https://docs.ray.io/en/master/serialization.html#troubleshooting \"\u001b[0m \u001b[0;31m# noqa\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 292\u001b[0m \"for more information.\")\n\u001b[0;32m--> 293\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 294\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 295\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_last_export_session_and_job\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mworker\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent_session_and_job\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: Could not serialize the function 1686926749.steps_1_through_4. Check https://docs.ray.io/en/master/serialization.html#troubleshooting for more information." ] } ], "source": [ "doc_html = articles.iloc[1][\"html\"]\n", "\n", "future = steps_1_through_4.remote(doc_html)\n", "step_4_results = ray.get(future)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Oops, that didn't work! Ray tasks can only operate over Python objects that can be serialized. A connection to Watson NLU can't be serialized because the connection object has open sockets and lock objects\n", "\n", "We also can't serialize the language model inside the SpaCy dependency parser (as of SpaCy version 3.0), because that Python object contains locks. Even if you could serialize it, doing so would result in Ray shipping a 500 megabyte model to every copy of the task, which would lead to underwhelming performance.\n", "\n", "To make this work, we'll need to pull the initialization code for both models inside the Ray task:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "@ray.remote\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " fix_spacy_warnings() # Workaround for spurious warning messages\n", " spacy_language_model = spacy.load(\"en_core_web_trf\")\n", " nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", " nlu_api.set_service_url(service_url)\n", " \n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now our Ray task works on a single document:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
\n", "
" ], "text/plain": [ " person \\\n", "0 [1977, 1991): 'Wendi Whitmore' \n", "\n", " title \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... " ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "step_4_results = ray.get(steps_1_through_4.remote(doc_html))\n", "step_4_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To process our entire collection of documents, we modify our `for` loop so that it spawns background tasks for each of the documents instead of processing the documents locally. Then we wrap the `for` loop in a call to `ray.get()`, which tells Ray to wait until all the background tasks have completed and collect the results:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:27:28,089\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "# Don't include this cell in the blog\n", "reboot_ray()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[2m\u001b[36m(steps_1_through_4 pid=69306)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69317)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69307)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69312)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69313)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69304)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69315)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69314)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69304)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69315)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69310)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69314)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(steps_1_through_4 pid=69310)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "CPU times: user 3.04 s, sys: 1.17 s, total: 4.22 s\n", "Wall time: 1min 26s\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
0[1281, 1292): 'Rob DiCicco'[1294, 1348): 'PharmD, Deputy Chief Health Off...
0[1213, 1229): 'Christoph Herman'[1231, 1281): 'SVP and Head of SAP HANA Enterp...
1[2227, 2242): 'Stephen Leonard'[2244, 2282): 'General Manager, IBM Cognitive ...
0[2290, 2298): 'Bob Lord'[2300, 2376): 'IBM Senior Vice President of Co...
.........
0[3113, 3123): 'Mike Doran'[3125, 3156): 'Worldwide Sales Director at IBM'
0[3156, 3170): 'Howard Boville'[3172, 3211): 'Senior Vice President, IBM Hybr...
0[3116, 3139): 'Samuel Brack Co-Founder'[3131, 3154): '-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
\n", "

260 rows × 2 columns

\n", "
" ], "text/plain": [ " person \\\n", "0 [1977, 1991): 'Wendi Whitmore' \n", "0 [1281, 1292): 'Rob DiCicco' \n", "0 [1213, 1229): 'Christoph Herman' \n", "1 [2227, 2242): 'Stephen Leonard' \n", "0 [2290, 2298): 'Bob Lord' \n", ".. ... \n", "0 [3113, 3123): 'Mike Doran' \n", "0 [3156, 3170): 'Howard Boville' \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [1488, 1498): 'Ana Zamper' \n", "\n", " title \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... \n", "0 [1294, 1348): 'PharmD, Deputy Chief Health Off... \n", "0 [1231, 1281): 'SVP and Head of SAP HANA Enterp... \n", "1 [2244, 2282): 'General Manager, IBM Cognitive ... \n", "0 [2300, 2376): 'IBM Senior Vice President of Co... \n", ".. ... \n", "0 [3125, 3156): 'Worldwide Sales Director at IBM' \n", "0 [3172, 3211): 'Senior Vice President, IBM Hybr... \n", "0 [3131, 3154): '-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "# NOTE: The blog version of this listing should highlight what has changed \n", "# relative to the original code.\n", "\n", "@ray.remote\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " fix_spacy_warnings() # Workaround for spurious warning messages\n", " nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", " nlu_api.set_service_url(service_url)\n", " spacy_language_model = spacy.load(\"en_core_web_trf\")\n", " \n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n", "\n", "# Repeat steps 1-4 on every document, at the same time\n", "dataframes_to_stack = ray.get([\n", " steps_1_through_4.remote(doc_html) for doc_html in articles[\"html\"]\n", "])\n", "\n", "# Step 5: Merge the results across documents\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This version runs correctly, and it runs in about 90 seconds on our 8-core laptop -- an additional performance improvement of almost 3x on top of the 3x improvement from Part 3.\n", "\n", "But there's room for two kinds of additional improvement:\n", "* This code loads a 500 megabyte model for every document, which incurs a signficant additional cost. The 8 cores on our test machine are fully occupied, but we only see a speedup of 3. So we're using more than twice as many CPU cycles per document.\n", "* This code produces several screens of scary log messages like: \n", " ```\n", " (pid=8210) Request failed 2 times; retrying in 2 sec\n", " ```\n", " because we're exceeding the request rate limit of our free Lite instance of Watson Natural Language Understanding. The limit for a Lite instance is 5 requests per second.\n", " We should be able to finish our 190 documents in 38 seconds while staying below the limit.\n", " But instead we're taking twice as long while continually bumping up against the limit and having to retry requests.\n", " \n", "We can use Ray *actors* to address both of these problems." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using actors to avoid repeated model startup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A Ray *actor* is persistent Python or Java object that lives in a Ray worker process. Actors can maintain arbitrary amounts of state, both mutable and immutable. If we attach the SpaCy language model to an actor, then the model will be initialized once in the actor's constructor instead of being loaded each time we process a document.\n", "\n", "Turning our `steps_1_through_4()` function into a Ray Actor is a simple matter of restructuring the code slightly.\n", "\n", "Here's the code before using actor:\n", "```python\n", "@ray.remote\n", "def steps_1_through_4(doc_html: str) -> pd.DataFrame:\n", " spacy_language_model = spacy.load(\"en_core_web_trf\")\n", " nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", " nlu_api.set_service_url(service_url)\n", " \n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_1_results[\"analyzed_text\"],\n", " spacy_language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n", "```\n", "\n", "And here's a version that uses an actor:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "@ray.remote\n", "class ParserModelActor(object):\n", " def __init__(self, spacy_model_name: str):\n", " self._language_model = spacy.load(spacy_model_name)\n", " self._nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", " self._nlu_api.set_service_url(service_url)\n", "\n", " def steps_1_through_4(self, doc_html: str) -> pd.DataFrame:\n", " fix_spacy_warnings() # Workaround for spurious warning messages\n", " step_1_results = mi.extract_named_entities_and_semantic_roles(doc_html, \n", " self._nlu_api)\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " self._language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Compared with the previous Ray task, this version makes two important changes:\n", "* The model initializaiton in first line of `steps_1_through_4` moves to the class's constructor.\n", "* We use the actor's local copy, `self._language_model`, as the input to the processing for step 3.\n", "\n", "Invoking a Ray actor is a two-step process. First, you create an instance of the actor. Then you tell that instance to perform tasks.\n", "\n", "Here's some code that creates an instance of the `ParserModelActor` Actor, invokes the actor's `steps_1_through_4` task, blocks until the task completes, and returns the results:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:29:03,824\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "# Don't include this cell in the blog\n", "reboot_ray()" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
\n", "
" ], "text/plain": [ " person \\\n", "0 [1977, 1991): 'Wendi Whitmore' \n", "\n", " title \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... " ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "actor = ParserModelActor.remote(\"en_core_web_trf\")\n", "future = actor.steps_1_through_4.remote(doc_html)\n", "step_4_results = ray.get(future)\n", "step_4_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This code runs, performs the computation in the background, and produces the correct result. And the actor will remain active as long as the Python variable `actor` is still in scope. We can pass additional documents to the `steps_1_through_4` task without incurring the overhead of loading a language model each time.\n", "\n", "As a Python class, this actor can only process one request at a time. To process documents in parallel, we can define a Ray [actor pool](https://docs.ray.io/en/latest/ray-core/actors/actor-utils.html#actor-pool), a group of multiple copies of our actor that can process documents in parallel." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:29:20,386\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "# Don't include this cell in the blog\n", "reboot_ray()" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69434)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69443)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69441)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69437)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69443)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69437)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69441)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69442)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69437)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69437)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69437)\u001b[0m \n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69441)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69443)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69438)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69442)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 2 times; retrying in 2 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69440)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69442)\u001b[0m Request failed 1 times; retrying in 1 sec\n", "\u001b[2m\u001b[36m(ParserModelActor pid=69429)\u001b[0m Request failed 3 times; retrying in 4 sec\n", "CPU times: user 1.52 s, sys: 651 ms, total: 2.17 s\n", "Wall time: 50.9 s\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1289, 1303): 'Wendi Whitmore'[1305, 1344): 'VP of Threat Intelligence, IBM ...
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
0[1213, 1229): 'Christoph Herman'[1231, 1281): 'SVP and Head of SAP HANA Enterp...
1[2227, 2242): 'Stephen Leonard'[2244, 2282): 'General Manager, IBM Cognitive ...
0[2290, 2298): 'Bob Lord'[2300, 2376): 'IBM Senior Vice President of Co...
.........
1[2119, 2134): 'James Kavanaugh'[2136, 2189): 'IBM senior vice president and c...
0[3116, 3139): 'Samuel Brack Co-Founder'[3131, 3154): '-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
0[1396, 1411): 'Sridhar Muppidi'[1413, 1451): 'Chief Technology Officer, IBM S...
\n", "

260 rows × 2 columns

\n", "
" ], "text/plain": [ " person \\\n", "0 [1289, 1303): 'Wendi Whitmore' \n", "0 [1977, 1991): 'Wendi Whitmore' \n", "0 [1213, 1229): 'Christoph Herman' \n", "1 [2227, 2242): 'Stephen Leonard' \n", "0 [2290, 2298): 'Bob Lord' \n", ".. ... \n", "1 [2119, 2134): 'James Kavanaugh' \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [1488, 1498): 'Ana Zamper' \n", "0 [1396, 1411): 'Sridhar Muppidi' \n", "\n", " title \n", "0 [1305, 1344): 'VP of Threat Intelligence, IBM ... \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... \n", "0 [1231, 1281): 'SVP and Head of SAP HANA Enterp... \n", "1 [2244, 2282): 'General Manager, IBM Cognitive ... \n", "0 [2300, 2376): 'IBM Senior Vice President of Co... \n", ".. ... \n", "1 [2136, 2189): 'IBM senior vice president and c... \n", "0 [3131, 3154): '-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "0 [1413, 1451): 'Chief Technology Officer, IBM S... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "# This listing should NOT appear in the blog version.\n", "# Run a pool with the actor defined above over the entire document collection, just\n", "# to make sure it works and to determine the running time.\n", "\n", "num_cpus = multiprocessing.cpu_count() // 2\n", "\n", "actors = ray.util.ActorPool([ParserModelActor.remote(\"en_core_web_trf\")\n", " for i in range(num_cpus)])\n", "\n", "# Repeat steps 1-4 on every document\n", "dataframes_to_stack = actors.map_unordered(\n", " lambda actor, value: actor.steps_1_through_4.remote(value), articles[\"html\"]\n", ")\n", "\n", "# Step 5: Merge the results across documents\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Using actors to manage request rate\n", "\n", "The large model that we attached to our `ParserModelActor` actor is an example of *immutable* actor state. Ray actors can also have *mutable* state that changes in response to tasks that the actor performs.\n", "\n", "For this application, we can use mutable actor state to track of how quickly our application is sending requests to the Watson Natural Language Understanding web service. \n", "\n", "Part 4 of this tutorial described in detail how to add such rate-limiting logic with a Ray actor. Here's a quick summary of how it works: We track the request rate is by tracking how much time has elapsed since the most recent request. With that information in hand, our actor can throttle new requests if they would exceed the rate limit. We put the logic for managing this state into an abstract base class, `RateLimitedActor`, the code for which can be found in `market_intelligence.py`. With that base class in place, we can define a Ray actor that sends documents to the Watson Natural Language Understanding web service while respecting a request rate limit. Because the Python API for Watson Natural Language Understanding is thread-safe, we can use a multithreaded Python actor to track multiple simulataneous requests." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "@ray.remote\n", "class NLUClientActor(mi.RateLimitedActor):\n", " \"\"\"\n", " Threaded actor to handle multiple simulatenous requests to the IBM Watson\n", " Natural Language Understanding service while respecting an upper bound on the\n", " number of requests per second.\n", " \"\"\"\n", " def __init__(self, requests_per_sec: float, \n", " api_key: str, service_url: str):\n", " super().__init__(requests_per_sec)\n", " # One instance of the Python API for all threads\n", " self._nlu_api = ibm_watson.NaturalLanguageUnderstandingV1(\n", " version=\"2021-01-01\", \n", " authenticator=ibm_cloud_sdk_core.authenticators.IAMAuthenticator(api_key))\n", " self._nlu_api.set_service_url(service_url)\n", " \n", " def process_internal(self, doc_html: str) -> Any:\n", " return mi.extract_named_entities_and_semantic_roles(doc_html, self._nlu_api)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "@ray.remote\n", "class ParserModelActor(object):\n", " def __init__(self, spacy_model_name: str, nlu_client: NLUClientActor):\n", " self._language_model = spacy.load(spacy_model_name)\n", " self._nlu_client = nlu_client\n", "\n", " def steps_1_through_4(self, doc_html: str) -> pd.DataFrame:\n", " fix_spacy_warnings() # Workaround for spurious warning messages\n", " step_1_results = ray.get(self._nlu_client.process.remote(doc_html))\n", " step_2_results = mi.identify_persons_quoted_by_name(step_1_results) \n", " step_3_results = mi.perform_targeted_dependency_parsing(\n", " step_2_results[\"person\"],\n", " self._language_model)\n", " step_4_results = mi.extract_titles_of_persons(step_2_results, step_3_results)\n", " return step_4_results\n" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:30:19,999\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "reboot_ray()" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 1.4 s, sys: 592 ms, total: 1.99 s\n", "Wall time: 48.1 s\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1597, 1605): 'Bob Lord'[1607, 1673): 'Senior Vice President, Cognitiv...
0[1977, 1991): 'Wendi Whitmore'[1993, 2040): 'Vice President, IBM X-Force Thr...
0[1289, 1303): 'Wendi Whitmore'[1305, 1344): 'VP of Threat Intelligence, IBM ...
0[1213, 1229): 'Christoph Herman'[1231, 1281): 'SVP and Head of SAP HANA Enterp...
1[2227, 2242): 'Stephen Leonard'[2244, 2282): 'General Manager, IBM Cognitive ...
.........
0[3116, 3139): 'Samuel Brack Co-Founder'[3131, 3154): '-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[497, 509): 'John Granger'[511, 620): 'Senior Vice President, Cloud Appl...
1[2375, 2386): 'Hamilton Yu'[2388, 2399): 'CEO of Taos'
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
\n", "

260 rows × 2 columns

\n", "
" ], "text/plain": [ " person \\\n", "0 [1597, 1605): 'Bob Lord' \n", "0 [1977, 1991): 'Wendi Whitmore' \n", "0 [1289, 1303): 'Wendi Whitmore' \n", "0 [1213, 1229): 'Christoph Herman' \n", "1 [2227, 2242): 'Stephen Leonard' \n", ".. ... \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [497, 509): 'John Granger' \n", "1 [2375, 2386): 'Hamilton Yu' \n", "0 [1488, 1498): 'Ana Zamper' \n", "\n", " title \n", "0 [1607, 1673): 'Senior Vice President, Cognitiv... \n", "0 [1993, 2040): 'Vice President, IBM X-Force Thr... \n", "0 [1305, 1344): 'VP of Threat Intelligence, IBM ... \n", "0 [1231, 1281): 'SVP and Head of SAP HANA Enterp... \n", "1 [2244, 2282): 'General Manager, IBM Cognitive ... \n", ".. ... \n", "0 [3131, 3154): '-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [511, 620): 'Senior Vice President, Cloud Appl... \n", "1 [2388, 2399): 'CEO of Taos' \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "num_cpus = multiprocessing.cpu_count() // 2\n", "\n", "nlu_client = NLUClientActor.options(max_concurrency=5).remote(5.0, api_key, service_url)\n", "actors = ray.util.ActorPool([ParserModelActor.remote(\"en_core_web_trf\", nlu_client)\n", " for i in range(num_cpus)])\n", "\n", "dataframes_to_stack = actors.map_unordered(\n", " lambda actor, value: actor.steps_1_through_4.remote(value), \n", " articles[\"html\"]\n", ")\n", "\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now our pipeline runs in about 45 seconds without hitting the rate limit.\n", "\n", "And now the 5 documents per second rate limit of our Lite instance of Watson Natural Language Understanding the chief performance bottleneck. We can remove that bottleneck by switching to a Standard instance of the service with a simple one-line change." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "# Don't show this cell in the blog version\n", "standard_api_key = os.environ.get(\"STANDARD_API_KEY\")\n", "standard_service_url = os.environ.get(\"STANDARD_SERVICE_URL\") " ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2022-04-16 14:31:16,894\tINFO services.py:1374 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265\u001b[39m\u001b[22m\n" ] } ], "source": [ "reboot_ray()" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 1.54 s, sys: 592 ms, total: 2.13 s\n", "Wall time: 36.8 s\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
persontitle
0[1066, 1084): 'Guilherme Franklin'[1086, 1099): 'El Ordeño COO'
1[1881, 1898): 'Martín Hagelstrom'[1900, 1943): 'IBM Blockchain Leader for IBM L...
0[1281, 1292): 'Rob DiCicco'[1294, 1348): 'PharmD, Deputy Chief Health Off...
0[1338, 1348): 'Rob Thomas'[1350, 1380): 'general manager, IBM Data & AI'
0[1289, 1303): 'Wendi Whitmore'[1305, 1344): 'VP of Threat Intelligence, IBM ...
.........
0[3116, 3139): 'Samuel Brack Co-Founder'[3131, 3154): '-Founder and CTO at DIA'
1[3511, 3525): 'Hillery Hunter'[3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud'
0[315, 329): 'Arvind Krishna'[331, 371): 'IBM chairman and chief executive ...
1[2119, 2134): 'James Kavanaugh'[2136, 2189): 'IBM senior vice president and c...
0[1488, 1498): 'Ana Zamper'[1500, 1535): 'Ecosystem Leader, IBM Latin Ame...
\n", "

260 rows × 2 columns

\n", "
" ], "text/plain": [ " person \\\n", "0 [1066, 1084): 'Guilherme Franklin' \n", "1 [1881, 1898): 'Martín Hagelstrom' \n", "0 [1281, 1292): 'Rob DiCicco' \n", "0 [1338, 1348): 'Rob Thomas' \n", "0 [1289, 1303): 'Wendi Whitmore' \n", ".. ... \n", "0 [3116, 3139): 'Samuel Brack Co-Founder' \n", "1 [3511, 3525): 'Hillery Hunter' \n", "0 [315, 329): 'Arvind Krishna' \n", "1 [2119, 2134): 'James Kavanaugh' \n", "0 [1488, 1498): 'Ana Zamper' \n", "\n", " title \n", "0 [1086, 1099): 'El Ordeño COO' \n", "1 [1900, 1943): 'IBM Blockchain Leader for IBM L... \n", "0 [1294, 1348): 'PharmD, Deputy Chief Health Off... \n", "0 [1350, 1380): 'general manager, IBM Data & AI' \n", "0 [1305, 1344): 'VP of Threat Intelligence, IBM ... \n", ".. ... \n", "0 [3131, 3154): '-Founder and CTO at DIA' \n", "1 [3527, 3558): 'IBM Fellow, VP & CTO, IBM Cloud' \n", "0 [331, 371): 'IBM chairman and chief executive ... \n", "1 [2136, 2189): 'IBM senior vice president and c... \n", "0 [1500, 1535): 'Ecosystem Leader, IBM Latin Ame... \n", "\n", "[260 rows x 2 columns]" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "\n", "num_cpus = multiprocessing.cpu_count()\n", "\n", "# The blog version of this code listing should only show how the next line changes.\n", "nlu_client = NLUClientActor.options(max_concurrency=num_cpus).remote(\n", " 80.0, standard_api_key, standard_service_url)\n", "\n", "\n", "# Note that this call to remote() will start asynchronously loading the language models.\n", "actors = ray.util.ActorPool([ParserModelActor.remote(\"en_core_web_trf\", nlu_client)\n", " for i in range(num_cpus)])\n", "\n", "dataframes_to_stack = actors.map_unordered(\n", " lambda actor, value: actor.steps_1_through_4.remote(value), \n", " articles[\"html\"]\n", ")\n", "\n", "step_5_results = pd.concat(dataframes_to_stack)\n", "step_5_results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now our running time is down to 31 seconds, from our original running time of 852 seconds. That's a performance improvement of 27x!\n", "\n", "Our 8-core laptop is now the bottleneck. We can further improve running time by switching to a larger machine or a cluster of machines, with no code changes.\n", "\n", "On a larger machine from the IBM cloud, we can process these documents in **15 seconds**, which is **57 times faster** than the code we started with!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running time comparison\n", "\n", "Here's a detailed chart that compares the running times across the different ways of performing our task." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "time_data = {\n", " # All times in seconds\n", " \"for loop\": 852,\n", " \"Pandas\\noptimizations\": 283,\n", " \"+Ray task\": 90,\n", " \"+Ray actors\": 45, \n", " \"+standard\\nNLU\\ninstance\": 30, \n", " # Separate run on IBM Cloud machine with 56 cores.\n", " \"+cloud VM\": 15,\n", "}\n", "\n", "plt.figure(figsize=(8, 5))\n", "plt.bar(time_data.keys(), time_data.values())\n", "plt.ylabel(\"Running time (sec)\")\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# The previous chart, but comparing documents per second\n", "num_docs = len(articles.index)\n", "docs_per_sec = {\n", " k: num_docs/v for k, v in time_data.items()\n", "}\n", "plt.figure(figsize=(8, 5))\n", "plt.bar(docs_per_sec.keys(), docs_per_sec.values())\n", "plt.ylabel(\"Documents per second\")\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" } }, "nbformat": 4, "nbformat_minor": 4 }