-
-
Notifications
You must be signed in to change notification settings - Fork 717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Run local code remotely on a worker #4003
Comments
"explicit is better than implicit". :) This feels something like working in a remote ipython kernel, which I suppose does still work (although I haven't seen anyone do it for quite some time). |
What advantages would the additional syntax provide over submitting a function? Could it supply any debugging utility? |
I took the bait and may have gotten carried away. This probably isn't quite what you're looking for, but it's... something ;) |
Sounds a lot like ipyparallel, specifically the sync imports context manager and the px magic |
The advantages here would be ergonomic. In the past I've done some work where I am submitting work to a remote cluster where GPUs are available and the egdge/login node has no GPU. Having some syntactic sugar here would be helpful |
@eriknw It certainly shows that magic is possible. I'm curious, do you have any thoughts on how this might be used to provide a context-manager like experience? My objective here is to get away from constructing and then calling functions, and towards remote execution. The closest thing we have today in common usage are IPython magics (indeed, I think that IPyParallel had remote execution cell magics that were awesome). I'd love to find some way to achieve this, but in a way that didn't require an IPython kernel. |
As always, the big challenge with context managers is that they don't provide a separate Nevertheless, here are some (probably totally doable) syntax ideas that use context managers: with get_result as rv, remotely:
...
rv = ...
# rv is a Future
# specify the number or names of results we want to keep
with get_results(2) as (x, y), remotely:
...
x = ...
y = ...
# or
with get_results('x', 'y') as (x, y), remotely:
...
x = ...
y = ...
# x, y are now futures The key here is context manager chaining. We can come up with variations of this spelling, such as: with running_remotely as futures, get_results('x', 'y'):
...
# futures.x and futures.y are now Futures The magic is easier if we actually put the code we want to run inside of a function body, such as: @with_running_remotely
def rv():
...
rv = ...
# rv (obtained from the name of the function) is a Future
@with_running_remotely('x', 'y')
def futures():
...
x = ...
y = ...
# futures.x and futures.y are now Future objects Maybe something along one of these lines would provide for a convenient user experience. Thoughts? Reactions? I'm open to suggestions for how to improve any particular spelling to make it clearer. |
I've started to whip this up. I don't think it'll be too hard or take too long, but I have other things I'm doing today, so please be patient :) Here's the spelling I'm targeting: from somewhere import runcode
with runcode() as result, remotely:
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
result = df.sum().compute() Here, we get the name to retrieve, We can also be more specific and ask to get multiple results: x = 1
with runcode('y', 'result') as (y, result), remotely:
y = 2
result = x + y
# Or
with runcode('y', 'result') as results, remotely:
y = 2
result = x + y QuestionsWhat do you want to do about variables such as We have no constraints. We can send them to the remote worker, do the right thing with dask futures, or raise. Or let the user specify the desired behavior. Do you want the results to be dask futures or the final result (e.g., Futures seem more natural to me most of the time. Perhaps we could find suitable names if both behaviors are wanted. More variations and brainstormingAs a function decorator, use the return value: @runcode.remotely
def result():
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
return df.sum().compute() Specify where to run programmatically: if run_locally:
where = somewhere.locally
else:
where = somewhere.remotely
with runcode() as result, where:
.... Also support with runcode() as result, locally:
...
@runcode.locally
def result():
... Recall The common misspelling (missing with runcode() as result:
... If you only want to run the code and don't need the result, simply do: with runcode(), remotely:
... I'm not sold on the name Also, this will be pretty magical and experimental. It would probably be best for it to live outside If anybody hates (or loves) this spelling (and I'm sure some people will), feel free to chime in even if you don't think you can do so constructively 😎 |
Sorry for the slow response. I'm on vacation this week and checking github infrequently. (my apologies for restarting this conversation and then ghosting by the way). I'm excited by this. I have a couple of questions:
Qustions from Erik
I think that it's likely that people will want to reach outside of local scope for local variables. Sending state somehow, either as pickled state or as Dask futures seems fine to me.
So, this is interesting I think and gets to a larger question of how we handle repeated computations with runcode() as df:
df = dd.read_parquet(...)
result = df with runcode() as result:
result = df.groupby(...).mean().compute() In this case returning the result as a Dask future seems best. This saves us from having to pull back a result that we may not be able to deal with locally (because, for example, we don't have cudf installed or an appropriate GPU device locally). How to pass handles between remote blocks is an interesting question though. |
Thanks for the reply and interest!
Yes/maybe. We currently need
I agree: returning all named variables is not the end target. That was the easiest thing to do that let me demonstrate functionality and upload a package to PyPI that isn't name-squatting. I don't have a strong opinion on how to specify which result to get, or whether the result in |
Actually, with afar.run, remotely:
x = 1
with afar.run, remotely:
y = 2
z = x + y
result = z.result() I like your idea of using the final assignment as the only value to "return" by default. Note that We can still support Given your initial example, we could execute the code on a worker, and copy the result locally. Is this syntax clear for that behavior? with afar.run, locally:
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
result = df.sum().compute() Because we used We can also put arguments for from afar import remotely
on_gpus = remotely(resources={"GPU": 1})
with afar.run, on_gpus:
...
with afar.run(resources={GPU": 1}), remotely:
... I'm feeling a little better about this. There may be something useful (or at least convenient) here. |
This looks really slick to me: with afar.run, remotely:
x = 1
with afar.run, remotely:
y = 2
z = x + y
result = z.result()
I agree that having a place to support constraints would be good. I don't currently have thoughts on where is best. |
I like the warning by the way :) It's also impressive that it's 99 lines of code (although I suppose that innerscope handles a bit) |
oh, the trouble you get me into with your encouragement ;) I have this minimally working. You can I'm sure there are sharp corners and severe/weird limitations with It would be super-duper handy if we could easily and reliably modify |
Question: does the remote side need to have |
I was getting an error to that effect, however I suspect that it may be caused by trying to pull in and serialize local state |
It shouldn't need to, but, well, it may need to right now. Thanks for giving |
Oh wait, yeah, |
OK. Good to know. Also, big +1 on the name. |
It's fun! And it's scratching a long-standing itch of mine.
You are a particularly easy mark it turns out :) |
:-P I'm also fond of the name. Anyway, I added two more features to 0.1.1:
Your initial example can now be: import afar
on_gpus = afar.remotely(resources={"GPU": 1})
from afar.get, on_gpus:
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
result = df.sum().compute()
# Now use `result` directly. No need for `result.result()`! I don't know if I think my itch is satisfied. I'll probably broadcast this out a bit more, but I don't plan to work much more on it. Unless, you know, I'm encouraged to ;) |
I'll play around and see if I can find reasons to keep you engaged.
Hopefully not.
I may also broadcast it out myself.
…On Fri, Jul 16, 2021 at 7:18 PM Erik Welch ***@***.***> wrote:
:-P
I'm also fond of the name. from afar import ... sounds almost poetic to
me.
Anyway, I added two more features to 0.1.1:
- afar.get automatically gathers the data locally. No need for
.result()
- remotely(**kwargs) passes keyword arguments to client.submit
Your initial example can now be:
import afar
on_gpus = afar.remotely(resources={"GPU": 1})
from afar.get, on_gpus:
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
result = df.sum().compute()
# Now use `result` directly. No need for `result.result()`!
I don't know if afar.get is the write word to use, but we'll see. It's
short.
I think my itch is satisfied. I'll probably broadcast this out a bit more,
but I don't plan to work much more on it. Unless, you know, I'm encouraged
to ;)
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#4003 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKZTGRDLHFE5VFRAOMUKTTYDD5TANCNFSM4PQL6M2Q>
.
|
*if you're ok with that that is
…On Fri, Jul 16, 2021 at 7:19 PM Matthew Rocklin ***@***.***> wrote:
I'll play around and see if I can find reasons to keep you engaged.
Hopefully not.
I may also broadcast it out myself.
On Fri, Jul 16, 2021 at 7:18 PM Erik Welch ***@***.***>
wrote:
> :-P
>
> I'm also fond of the name. from afar import ... sounds almost poetic to
> me.
>
> Anyway, I added two more features to 0.1.1:
>
> - afar.get automatically gathers the data locally. No need for
> .result()
> - remotely(**kwargs) passes keyword arguments to client.submit
>
> Your initial example can now be:
>
> import afar
> on_gpus = afar.remotely(resources={"GPU": 1})
> from afar.get, on_gpus:
> import dask_cudf
> df = dask_cudf.read_parquet("s3://...")
> result = df.sum().compute()
> # Now use `result` directly. No need for `result.result()`!
>
> I don't know if afar.get is the write word to use, but we'll see. It's
> short.
>
> I think my itch is satisfied. I'll probably broadcast this out a bit
> more, but I don't plan to work much more on it. Unless, you know, I'm
> encouraged to ;)
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <#4003 (comment)>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AACKZTGRDLHFE5VFRAOMUKTTYDD5TANCNFSM4PQL6M2Q>
> .
>
|
Will do! Give me a few minutes. |
Done. I also set up CI, which took a bit longer than expected (b/c had to fix some things). But, tests now pass for Python 3.7, 3.8, 3.9, and PyPy--hooray! |
0.1.3 released. Since we can't always update the locals of a frame (and I'm not sure I want to write the hack to do so), it may be more convenient at times to use our own mapping. For example: def some_func():
...
run = afar.run(data={"a": 1})
with run, remotely:
b = a + 1
# b doesn't exist locally, because we can't update `frame.f_locals`
assert run.data["b"].result() == 2
# continue using the data...
with run, remotely:
c = a + b Note that the singleton And for warm-fuzzies, I now test on Linux, OS X, and Windows, and with pip and conda (I was seeing differences between these two). |
Another suggestion, if the last statement isn't an assignment, but just an expression like the following: with afar.run, remotely:
df Maybe we should call |
Yeah, that's pretty atypical, but so is This shows I can return the final expression and display it (but it's not 100% reliable yet). What I don't know is how to compute the appropriate repr remotely and copy that instead of the original object. Would you expect anything else do be done with the final statement? Should this be made available to the user somehow, or is the repr enough? |
Also, I'd wait a bit before broadcasting this out. I'm testing and fixing bugs. |
Yup. No problem. I'm iterating with a GPU workload on Coiled and it's interesting getting both smooth at the same time. FWIW this is already a very useful tool for me. I'm finding that GPUs-on-the-cloud feel closer at hand already. |
Great to hear!
See release notes here: https://github.com/eriknw/afar/releases Notably, you can now look at Also, added Note that the main limitation of |
Update: %load_ext afar %%afar
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
result = df.sum().compute() instead of the original def f():
import dask_cudf
df = dask_cudf.read_parquet("s3://...")
return df.sum().compute()
result = client.submit(f).result() More examples: %%afar x, y # save both x and y as Dask Futures
x = 1
y = x + 1 z = %afar x + y or %afar z = x + y I think this is starting to get pretty nice. |
Ah great, I look forward to taking them for a spin. Thanks @eriknw! |
I find myself often wanting to run code on a worker, rather than on my local client. This happens in a few settings:
dd.read_parquet
remotely (cc @martindurant @jcrist )Today I can do this by writing a function and submitting that function as a task
It might make sense to provide syntax around this to make it more magical (or it might not). We might do something like the following:
I know that @eriknw has done magic like this in the past. We could enlist this help. However, we may not want to do this due to the magical and novel behavior.
The text was updated successfully, but these errors were encountered: