Using Parallel Magics

IPython has a few magics for working with your engines.

This assumes you have started an IPython cluster, either with the notebook interface, or the ipcluster/controller/engine commands.

In [1]:
%pylab inline
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
In [2]:
from IPython import parallel
rc = parallel.Client()
dv = rc[:]
rc.ids
Out [2]:
[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.

Now we can execute code remotely with %px:

In [3]:
%px a=5
In [4]:
%px print a
[stdout:0] 5
[stdout:1] 5
[stdout:2] 5
[stdout:3] 5
In [5]:
%px a
Out[0:3]: 5
Out[1:3]: 5
Out[2:3]: 5
Out[3:3]: 5
In [6]:
with dv.sync_imports():
    import sys
importing sys on engine(s)

stderr comes to stderr

In [7]:
%px print >> sys.stderr, "ERROR"
[stderr:0] ERROR
[stderr:1] ERROR
[stderr:2] ERROR
[stderr:3] ERROR

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

In [8]:
%pxconfig --noblock
In [9]:
%px import time
%px time.sleep(3*rank)
%px time.time()
Out [9]:
<AsyncResult: execute>

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:

In [10]:
%pxresult

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

In [11]:
%pxconfig --block
%px %pylab inline
[stdout:0] 
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
[stdout:1] 
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
[stdout:2] 
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
[stdout:3] 
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.

%%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.

In [12]:
dv.scatter('rank', dv.targets, flatten=True)
Out [12]:
<AsyncResult: scatter>
In [13]:
%%px --noblock
x = linspace(0,pi,1000)
plt.plot(x,sin((rank+1)*x))
plt.title("Plot %i" % rank)
Out [13]:
<AsyncResult: execute>
In [14]:
%pxresult
[output:0]
[output:1]
[output:2]
[output:3]
Out[0:8]: <matplotlib.text.Text at 0x106a18dd0>
Out[1:8]: <matplotlib.text.Text at 0x1103bddd0>
Out[2:8]: <matplotlib.text.Text at 0x11103ddd0>
Out[3:8]: <matplotlib.text.Text at 0x102c81dd0>

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

The choices are:

In [15]:
%%px --group-outputs=engine
x = linspace(0,pi,1000)
plt.plot(x,sin((rank+1)*x))
plt.title("Plot %i" % rank)
[output:0]
Out[0:9]: <matplotlib.text.Text at 0x106afd750>
[output:1]
Out[1:9]: <matplotlib.text.Text at 0x1104a33d0>
[output:2]
Out[2:9]: <matplotlib.text.Text at 0x1111233d0>
[output:3]
Out[3:9]: <matplotlib.text.Text at 0x102d673d0>

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:

In [16]:
%%px --noblock

import os
from IPython.display import display, Math

display(Math("sin(x^%i)" % (rank+1)))

x = linspace(0,pi,1000)
plt.plot(x,sin(x**(rank+1)))
plt.title("Plot %i" % rank)
Out [16]:
<AsyncResult: execute>
In [17]:
%pxresult --group-outputs=order
[output:0]
$$sin(x^1)$$
[output:1]
$$sin(x^2)$$
[output:2]
$$sin(x^3)$$
[output:3]
$$sin(x^4)$$
[output:0]
[output:1]
[output:2]
[output:3]
Out[0:10]: <matplotlib.text.Text at 0x106bbd290>
Out[1:10]: <matplotlib.text.Text at 0x110561dd0>
Out[2:10]: <matplotlib.text.Text at 0x1111e1dd0>
Out[3:10]: <matplotlib.text.Text at 0x102e25dd0>

Single-engine views

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

In [18]:
def generate_output():
    """function for testing output
    
    publishes two outputs of each type, and returns something
    """
    
    import sys,os
    from IPython.core.display import display, HTML, Math
    
    print "stdout"
    print >> sys.stderr, "stderr"
    
    display(HTML("<a href='http://ipython.org'>Visit IPython</a>"))
    
    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'.

In [19]:
e0 = rc[-1]
e0.block = True
e0.activate('0')
In [20]:
%px0 generate_output()
stdout
stderr
Out[3:11]: 11036
In [21]:
%px generate_output()
[stdout:0] stdout
[stdout:1] stdout
[stdout:2] stdout
[stdout:3] stdout
[stderr:0] stderr
[stderr:1] stderr
[stderr:2] stderr
[stderr:3] stderr
[output:0]
[output:1]
[output:2]
[output:3]
Out[0:11]: 11030
Out[1:11]: 11027
Out[2:11]: 11026
Out[3:12]: 11036

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

In [22]:
%pxresult --group-outputs engine
[stdout:0] stdout
[stdout:1] stdout
[stdout:2] stdout
[stdout:3] stdout
[stderr:0] stderr
[output:0]
Out[0:11]: 11030
[stderr:1] stderr
[output:1]
Out[1:11]: 11027
[stderr:2] stderr
[output:2]
Out[2:11]: 11026
[stderr:3] stderr
[output:3]
Out[3:12]: 11036

Parallel Exceptions

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

In [23]:
%%px
from numpy.random import random
A = random((100,100,'invalid shape'))
[0:execute]: 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)<ipython-input-12-2f86496ae392> in <module>()
      1 from numpy.random import random
----> 2 A = random((100,100,'invalid shape'))
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.RandomState.random_sample (numpy/random/mtrand/mtrand.c:6136)()
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.cont0_array (numpy/random/mtrand/mtrand.c:1571)()
ValueError: negative dimensions are not allowed

[1:execute]: 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)<ipython-input-12-2f86496ae392> in <module>()
      1 from numpy.random import random
----> 2 A = random((100,100,'invalid shape'))
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.RandomState.random_sample (numpy/random/mtrand/mtrand.c:6136)()
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.cont0_array (numpy/random/mtrand/mtrand.c:1571)()
ValueError: negative dimensions are not allowed

[2:execute]: 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)<ipython-input-12-2f86496ae392> in <module>()
      1 from numpy.random import random
----> 2 A = random((100,100,'invalid shape'))
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.RandomState.random_sample (numpy/random/mtrand/mtrand.c:6136)()
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.cont0_array (numpy/random/mtrand/mtrand.c:1571)()
ValueError: negative dimensions are not allowed

[3:execute]: 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)<ipython-input-13-2f86496ae392> in <module>()
      1 from numpy.random import random
----> 2 A = random((100,100,'invalid shape'))
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.RandomState.random_sample (numpy/random/mtrand/mtrand.c:6136)()
/Users/minrk/dev/py/numpy/numpy/random/mtrand.so in mtrand.cont0_array (numpy/random/mtrand/mtrand.c:1571)()
ValueError: negative dimensions are not allowed

Engines are IPython

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

In [24]:
%%px
%%timeit
from numpy.random import random
from numpy.linalg import norm
A = random((100*(rank+1),100*(rank+1)))
norm(A,2)
[stdout:0] 100 loops, best of 3: 3.8 ms per loop
[stdout:1] 10 loops, best of 3: 20.6 ms per loop
[stdout:2] 10 loops, best of 3: 47.5 ms per loop
[stdout:3] 10 loops, best of 3: 85.6 ms per loop

And you can run line magics remotely as well

In [25]:
%%px
import os
from numpy.random import random
from numpy.linalg import norm

A = random((200*(rank+1),200*(rank+1)))

pid = os.getpid()
!echo 'hello from $pid'

%timeit norm(A,2)
[stdout:0] 
hello from 11030
10 loops, best of 3: 39.4 ms per loop
[stdout:1] 
hello from 11027
1 loops, best of 3: 356 ms per loop
[stdout:2] 
hello from 11026
1 loops, best of 3: 762 ms per loop
[stdout:3] 
hello from 11036
1 loops, best of 3: 788 ms per loop

Bonus Points

Parallel magics, line magics, display protocol, and cell magics all come together for parallel remote plots in R:

In [26]:
X = np.linspace(0, 3*np.pi, 32)
Y = np.sin(X)
dv.scatter('X', X)
dv.scatter('Y', Y)
plot(X,Y, '-o')
Out [26]:
[<matplotlib.lines.Line2D at 0x1044e1110>]
In [27]:
%px %load_ext rmagic
%px %Rpush X Y
In [28]:
%%px
%%R -i X,Y -o XYcoef
XYlm = lm(Y~X)
XYcoef = coef(XYlm)
print(summary(XYlm))
par(mfrow=c(2,2))
plot(XYlm)
[output:0]
Call:
lm(formula = Y ~ X)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.27457 -0.10157  0.02908  0.14237  0.19716 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)   
(Intercept)  0.23043    0.12571   1.833  0.11648   
X            0.41951    0.09884   4.244  0.00541 **
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 0.1947 on 6 degrees of freedom
Multiple R-squared: 0.7502,	Adjusted R-squared: 0.7085 
F-statistic: 18.01 on 1 and 6 DF,  p-value: 0.005414 

[output:1]
Call:
lm(formula = Y ~ X)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.093232 -0.063326 -0.006248  0.049726  0.142943 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  2.56530    0.15735   16.30 3.39e-06 ***
X           -0.81062    0.04414  -18.37 1.68e-06 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 0.08696 on 6 degrees of freedom
Multiple R-squared: 0.9825,	Adjusted R-squared: 0.9796 
F-statistic: 337.3 on 1 and 6 DF,  p-value: 1.679e-06 

[output:2]
Call:
lm(formula = Y ~ X)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.093232 -0.063326 -0.006248  0.049726  0.142943 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -5.07457    0.26346  -19.26 1.27e-06 ***
X            0.81062    0.04414   18.37 1.68e-06 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 0.08696 on 6 degrees of freedom
Multiple R-squared: 0.9825,	Adjusted R-squared: 0.9796 
F-statistic: 337.3 on 1 and 6 DF,  p-value: 1.679e-06 

[output:3]
Call:
lm(formula = Y ~ X)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.27457 -0.10157  0.02908  0.14237  0.19716 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)   
(Intercept)  4.18419    0.82922   5.046  0.00234 **
X           -0.41951    0.09884  -4.244  0.00541 **
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 

Residual standard error: 0.1947 on 6 degrees of freedom
Multiple R-squared: 0.7502,	Adjusted R-squared: 0.7085 
F-statistic: 18.01 on 1 and 6 DF,  p-value: 0.005414 

And the same for parallel remote Cython

In [29]:
%px %load_ext cythonmagic
In [30]:
%%px
%%cython
cimport cython
from libc.math cimport exp, sqrt, pow, log, erf

@cython.cdivision(True)
cdef double std_norm_cdf(double x) nogil:
    return 0.5*(1+erf(x/sqrt(2.0)))

@cython.cdivision(True)
def black_scholes(double s, double k, double t, double v,
                 double rf, double div, double cp):
    """Price an option using the Black-Scholes model.
    
    s : initial stock price
    k : strike price
    t : expiration time
    v : volatility
    rf : risk-free rate
    div : dividend
    cp : +1/-1 for call/put
    """
    cdef double d1, d2, optprice
    with nogil:
        d1 = (log(s/k)+(rf-div+0.5*pow(v,2))*t)/(v*sqrt(t))
        d2 = d1 - v*sqrt(t)
        optprice = cp*s*exp(-div*t)*std_norm_cdf(cp*d1) - \
            cp*k*exp(-rf*t)*std_norm_cdf(cp*d2)
    return optprice
In [31]:
dv.scatter('call', [50., 100., 150., 200.], flatten=True)
%px %timeit black_scholes(call, 100.0, 1.0, 0.3, 0.03, 0.0, -1)
%px black_scholes(call, 100.0, 1.0, 0.3, 0.03, 0.0, -1)
[stdout:0] 100000 loops, best of 3: 3.52 us per loop
[stdout:1] 100000 loops, best of 3: 2.82 us per loop
[stdout:2] 100000 loops, best of 3: 3.47 us per loop
[stdout:3] 100000 loops, best of 3: 4.25 us per loop
Out[0:21]: 47.14277550628262
Out[1:21]: 10.327861752731728
Out[2:21]: 1.1740720290751039
Out[3:22]: 0.10913979989888967

Last, but not least, opening a QtConsole onto an engine

In [32]:
%px0 %qtconsole