Using Parallel Magics#

IPython has a few magics to make working with your engines a bit nicer in IPython, e.g. via a Jupyter notebook.

As always, first we start a cluster (or connect to an existing one):

import ipyparallel as ipp

rc = ipp.Cluster(n=4).start_and_connect_sync()
dv = rc[:]
rc.ids
Using existing profile dir: '/Users/benjaminrk/.ipython/profile_default'
Starting 4 engines with <class 'ipyparallel.cluster.launcher.LocalEngineSetLauncher'>
[0, 1, 2, 3]

Creating a Client registers the parallel magics %px, %%px, %pxresult, %pxconfig, and %autopx.
These magics are initially associated with a DirectView, always associated with all currently registered engines.

First, we can execute single lines remotely with %px:

%px a=5
%px print(a)
[stdout:0] 5
[stdout:1] 5
[stdout:2] 5
[stdout:3] 5
%px a
Out[2:3]: 5
Out[0:3]: 5
Out[3:3]: 5
Out[1:3]: 5
with dv.sync_imports():
    import sys
importing sys on engine(s)
%px from __future__ import print_function
%px print("ERROR", file=sys.stderr)
[stderr:0] ERROR
[stderr:1] ERROR
[stderr:3] ERROR
[stderr:2] ERROR

You don’t have to wait for results. The %pxconfig magic lets you change the default blocking/targets for the %px magics:

%pxconfig --noblock
%px import time
%px time.sleep(1)
%px time.time()
<AsyncResult(%px): pending>

But you will notice that this didn’t output the result of the last command. For this, we have %pxresult, which displays the output of the latest request:

%pxresult
Out[0:8]: 1635248507.7749069
Out[1:8]: 1635248507.7802722
Out[2:8]: 1635248507.780295
Out[3:8]: 1635248507.780204

Remember, an IPython engine is IPython, so you can do magics remotely as well!

%pxconfig --block
%px %matplotlib inline

%%px can be used to lower the priority of the engines to improve system performance under heavy CPU load.

%%px
import psutil
psutil.Process().nice(20 if psutil.POSIX else psutil.IDLE_PRIORITY_CLASS)
%%px
import numpy as np
import matplotlib.pyplot as plt

%%px can also be used as a cell magic, for submitting whole blocks. This one acceps --block and --noblock flags to specify the blocking behavior, though the default is unchanged.

dv.scatter('id', dv.targets, flatten=True)
dv['stride'] = len(dv)
%%px --noblock
x = np.linspace(0,np.pi,1000)
for n in range(id,12, stride):
    print(n)
    plt.plot(x,np.sin(n*x))
plt.title("Plot %i" % id)
<AsyncResult(%px): pending>
%pxresult
[stdout:0] 
0
4
8
[stdout:1] 
1
5
9
[stdout:2] 
2
6
10
[stdout:3] 
3
7
11
[output:0]
../_images/9f08b087a0d8cd38bc83cb3626a262fc48e35b991262fc5e201866835e913628.png
[output:1]
../_images/d1937163bfc1bd9833ae762eef203bef187e103415fb04dd996f28103f615241.png
[output:2]
../_images/fa9fbbca98c7799883894d329f0bde9fa8a894af4e61c39364d901426a1d7b0f.png
[output:3]
../_images/29806b81a0b145df6d56c51ea9d575878997400531b0ba53f68f4a093aa5e79f.png
Out[0:13]: Text(0.5, 1.0, 'Plot 0')
Out[1:13]: Text(0.5, 1.0, 'Plot 1')
Out[2:13]: Text(0.5, 1.0, 'Plot 2')
Out[3:13]: Text(0.5, 1.0, 'Plot 3')

It also lets you choose some amount of the grouping of the outputs with --group-outputs:

The choices are:

  • engine - all of an engine’s output is collected together

  • type - where stdout of each engine is grouped, etc. (the default)

  • order - same as type, but individual displaypub outputs are interleaved. That is, it will output the first plot from each engine, then the second from each, etc.

%%px --group-outputs=engine
x = np.linspace(0,np.pi,1000)
for n in range(id+1,12, stride):
    print(n)
    plt.figure()
    plt.plot(x,np.sin(n*x))
    plt.title("Plot %i" % n)
[stdout:0] 1
5
9
[stdout:1] 2
6
10
[stdout:2] 3
7
11
[stdout:3] 4
8
[output:3]
../_images/902d748b5bd0672fcc44e10845ce49f11528f307a64c9bc22c614104beb5cdb9.png
[output:0]
../_images/cdb3b938a91f7e497aa29e4d30d1cc391903fe01d815c255929ddc638a25de6b.png
[output:1]
../_images/834c84ca7e6698402c408a0b3e24483b7832aa549836517eb19f7cb9a1753b38.png
[output:2]
../_images/54f6e6806faa00f689fd13bcda9501a89aaf928988dd027a96bfafcb191703b1.png
[output:3]
../_images/bdddf89ac7e1f2455b3cbc555e3d0b715af55f3ebd3ef6ea57a2d91e78566dc2.png
[output:0]
../_images/5b393ac215da09a48a70fbb4168a0d726654e50b7d5463f574b84256c3009f7b.png
[output:1]
../_images/bd789d7021530dbfc3d455ab032acfcfd1034bea17f57cfed6968085a27e39ad.png
[output:2]
../_images/0ce8815e7031d84505362c805df7602eda89f74fbd8aa60f6b8eb33c9aec2334.png
[output:0]
../_images/724c8308ad50cdb1f3c86d870206a35499ea2e4ad09b36b730d19b19959d4662.png
[output:1]
../_images/f1af8b11c9065f9cb0b2d2ad0a1298fcf2104420c502fcc699023705fbe932e7.png
[output:2]
../_images/d8205b7bc67d5fb32d759df998deb74dd4e67c499a7edb159569d506b0224383.png

When you specify ‘order’, then individual display outputs (e.g. plots) will be interleaved.

%pxresult takes the same output-ordering arguments as %%px, so you can view the previous result in a variety of different ways with a few sequential calls to %pxresult:

%pxresult --group-outputs=order
[stdout:0] 
1
5
9
[stdout:1] 
2
6
10
[stdout:2] 
3
7
11
[stdout:3] 
4
8
[output:0]
../_images/cdb3b938a91f7e497aa29e4d30d1cc391903fe01d815c255929ddc638a25de6b.png
[output:1]
../_images/834c84ca7e6698402c408a0b3e24483b7832aa549836517eb19f7cb9a1753b38.png
[output:2]
../_images/54f6e6806faa00f689fd13bcda9501a89aaf928988dd027a96bfafcb191703b1.png
[output:0]
../_images/5b393ac215da09a48a70fbb4168a0d726654e50b7d5463f574b84256c3009f7b.png
[output:1]
../_images/bd789d7021530dbfc3d455ab032acfcfd1034bea17f57cfed6968085a27e39ad.png
[output:2]
../_images/0ce8815e7031d84505362c805df7602eda89f74fbd8aa60f6b8eb33c9aec2334.png
[output:0]
../_images/724c8308ad50cdb1f3c86d870206a35499ea2e4ad09b36b730d19b19959d4662.png
[output:1]
../_images/f1af8b11c9065f9cb0b2d2ad0a1298fcf2104420c502fcc699023705fbe932e7.png
[output:2]
../_images/d8205b7bc67d5fb32d759df998deb74dd4e67c499a7edb159569d506b0224383.png

Single-engine views#

When a DirectView has a single target, the output is a bit simpler (no prefixes on stdout/err, etc.):

from __future__ import print_function

def generate_output():
    """function for testing output
    
    publishes two outputs of each type, and returns something
    """
    
    import sys,os
    from IPython.display import display, HTML, Math
    
    print("stdout")
    print("stderr", file=sys.stderr)
    
    display(HTML("<b>HTML</b>"))
    
    print("stdout2")
    print("stderr2", file=sys.stderr)
    
    display(Math(r"\alpha=\beta"))
    
    return os.getpid()

dv['generate_output'] = generate_output

You can also have more than one set of parallel magics registered at a time.

The View.activate() method takes a suffix argument, which is added to 'px'.

e0 = rc[-1]
e0.block = True
e0.activate('0')
%px0 generate_output()
[stdout:3] stdout
stdout2
[stderr:3] stderr
stderr2
[output:3]
HTML
[output:3]
\[\displaystyle \alpha=\beta\]
Out[3:15]: 83115
%px generate_output()
[stdout:0] stdout
stdout2
[stdout:1] stdout
stdout2
[stdout:2] stdout
stdout2
[stderr:2] stderr
stderr2
[stdout:3] stdout
stdout2
[stderr:0] stderr
stderr2
[output:0]
HTML
[stderr:1] stderr
stderr2
[output:1]
HTML
[stderr:3] stderr
stderr2
[output:2]
HTML
[output:3]
HTML
[output:0]
\[\displaystyle \alpha=\beta\]
[output:1]
\[\displaystyle \alpha=\beta\]
Out[0:15]: 83112
[output:2]
\[\displaystyle \alpha=\beta\]
[output:3]
\[\displaystyle \alpha=\beta\]
Out[1:15]: 83113
Out[2:15]: 83114
Out[3:16]: 83115

As mentioned above, we can redisplay those same results with various grouping:

%pxresult --group-outputs order
[stdout:0] 
stdout
stdout2
[stdout:1] 
stdout
stdout2
[stdout:2] 
stdout
stdout2
[stdout:3] 
stdout
stdout2
[stderr:0] 
stderr
stderr2
[stderr:1] 
stderr
stderr2
[stderr:2] 
stderr
stderr2
[stderr:3] 
stderr
stderr2
[output:0]
HTML
[output:1]
HTML
[output:2]
HTML
[output:3]
HTML
[output:0]
\[\displaystyle \alpha=\beta\]
[output:1]
\[\displaystyle \alpha=\beta\]
[output:2]
\[\displaystyle \alpha=\beta\]
[output:3]
\[\displaystyle \alpha=\beta\]
Out[0:15]: 83112
Out[1:15]: 83113
Out[2:15]: 83114
Out[3:16]: 83115
%pxresult --group-outputs engine
[stdout:0] 
stdout
stdout2
[stderr:0] 
stderr
stderr2
[output:0]
HTML
\[\displaystyle \alpha=\beta\]
Out[0:15]: 83112
[stdout:1] 
stdout
stdout2
[stderr:1] 
stderr
stderr2
[output:1]
HTML
\[\displaystyle \alpha=\beta\]
Out[1:15]: 83113
[stdout:2] 
stdout
stdout2
[stderr:2] 
stderr
stderr2
[output:2]
HTML
\[\displaystyle \alpha=\beta\]
Out[2:15]: 83114
[stdout:3] 
stdout
stdout2
[stderr:3] 
stderr
stderr2
[output:3]
HTML
\[\displaystyle \alpha=\beta\]
Out[3:16]: 83115

Parallel Exceptions#

When you raise exceptions with the parallel exception, the CompositeError raised locally will display your remote traceback.

%%px
from numpy.random import random
A = random((100, 100, 'invalid shape'))
[0:execute]: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/9p/clj0fc754y35m01btd46043c0000gn/T/ipykernel_83112/1064401740.py in <module>
      1 from numpy.random import random
----> 2 A = random((100, 100, 'invalid shape'))

mtrand.pyx in numpy.random.mtrand.RandomState.random()

mtrand.pyx in numpy.random.mtrand.RandomState.random_sample()

_common.pyx in numpy.random._common.double_fill()

TypeError: 'str' object cannot be interpreted as an integer
[2:execute]: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/9p/clj0fc754y35m01btd46043c0000gn/T/ipykernel_83114/1064401740.py in <module>
      1 from numpy.random import random
----> 2 A = random((100, 100, 'invalid shape'))

mtrand.pyx in numpy.random.mtrand.RandomState.random()

mtrand.pyx in numpy.random.mtrand.RandomState.random_sample()

_common.pyx in numpy.random._common.double_fill()

TypeError: 'str' object cannot be interpreted as an integer
[1:execute]: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/9p/clj0fc754y35m01btd46043c0000gn/T/ipykernel_83113/1064401740.py in <module>
      1 from numpy.random import random
----> 2 A = random((100, 100, 'invalid shape'))

mtrand.pyx in numpy.random.mtrand.RandomState.random()

mtrand.pyx in numpy.random.mtrand.RandomState.random_sample()

_common.pyx in numpy.random._common.double_fill()

TypeError: 'str' object cannot be interpreted as an integer
[3:execute]: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/9p/clj0fc754y35m01btd46043c0000gn/T/ipykernel_83115/1064401740.py in <module>
      1 from numpy.random import random
----> 2 A = random((100, 100, 'invalid shape'))

mtrand.pyx in numpy.random.mtrand.RandomState.random()

mtrand.pyx in numpy.random.mtrand.RandomState.random_sample()

_common.pyx in numpy.random._common.double_fill()

TypeError: 'str' object cannot be interpreted as an integer
4 errors

Sometimes, an error will occur on just one engine, while the rest are still working.

When this happens, you will see errors immediately, and can interrupt the execution:

dv.scatter("rank", rc.ids, flatten=True)
<AsyncResult(scatter): pending>
%%px
import time
if rank == 0:
    raise RuntimeError("rank 0 failed!")
time.sleep(10)
    
[0:execute]: 
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
/var/folders/9p/clj0fc754y35m01btd46043c0000gn/T/ipykernel_83112/2811384868.py in <module>
      1 import time
      2 if rank == 0:
----> 3     raise RuntimeError("rank 0 failed!")
      4 time.sleep(10)
      5 

RuntimeError: rank 0 failed!
1 errors

Remote Cell Magics#

Remember, Engines are IPython too, so the cell that is run remotely by %%px can in turn use a cell magic.

%%px
%%timeit
from numpy.random import random
from numpy.linalg import norm
A = random((100, 100))
norm(A, 2)
[stdout:1] 6.01 ms ± 596 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[stdout:0] 6.02 ms ± 549 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[stdout:3] 6.06 ms ± 580 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[stdout:2] 6.08 ms ± 430 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Local Execution#

You can instruct %%px to also execute the cell locally. This is useful for interactive definitions, or if you want to load a data source everywhere, not just on the engines.

%%px --local
import os
thispid = os.getpid()
print(thispid)
83088
[stdout:0] 
83112
[stdout:1] 
83113
[stdout:2] 
83114
[stdout:3] 
83115