Skip to content

Add async support #193

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

Merged
merged 21 commits into from
Nov 9, 2023
Merged

Add async support #193

merged 21 commits into from
Nov 9, 2023

Conversation

mattt
Copy link
Contributor

@mattt mattt commented Nov 8, 2023

This PR adds support for async operations to the Replicate client. Namespace operations like predictions.list and models.create now have async variants with the async_ prefix (predictions.async_list and models.async_create).

Here's an example of what that looks like in practice:

import replicate

model = await replicate.models.async_get("stability-ai/sdxl")

input = {
  "prompt": "A chariot pulled by a team of rainbow unicorns, driven by an astronaut, dramatic lighting",
}

output = await replicate.async_run(f"stability-ai/sdxl:{model.latest_version.id}", input)
Output

One of the most common questions I hear is how to run a bunch of predictions in parallel. The async functionality provided by this PR makes it really straightforward:

import asyncio
import replicate

# https://replicate.com/stability-ai/sdxl
model_version = "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b"
prompts = [
    f"A chariot pulled by a team of {count} rainbow unicorns"
    for count in ["two", "four", "six", "eight"]
]

async with asyncio.TaskGroup() as tg:
    tasks = [
        tg.create_task(replicate.async_run(model_version, input={"prompt": prompt}))
        for prompt in prompts
    ]

results = await asyncio.gather(*tasks)
print(results)

Under the hood, Client manages an httpx.Client and an httpx.AsyncClient, which handle calls to _request and _async_request, respectively. Both are created lazily, so API consumers using only sync or only async functionality won't be affected by functionality they aren't using.

Implementation-wise, sync and async variants have separate code paths. This creates nontrivial amounts of duplication, but its benefits to clarity and performance justify those costs. For instance, it'd have been nice if the sync variant were implemented as a blocking call to the async variant, but that would require starting an event loop, which has additional overhead and causes problems if done within an existing event loop.

Alternative to #76
Resolves #145
Resolves #107
Resolves #74

mattt added 14 commits November 8, 2023 06:11
Remove workaround of deleting version field from response JSON in _prepare_model

Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
@mattt mattt marked this pull request as ready for review November 8, 2023 20:03
mattt added 3 commits November 8, 2023 12:41
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
@mattt mattt requested a review from zeke November 8, 2023 21:54
@@ -48,6 +49,7 @@ disable = [
"W0622", # Redefining built-in
"R0903", # Too few public methods
]
good-names = ["id"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does good-names mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're good names, Zeke.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(But seriously, this is to stop pylint from complaining about variables named id)

Copy link
Member

@zeke zeke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow this looks like it was a lot of work.

async variants with the async_ prefix

Is this a cow-path convention in Python land for async variants of synchronous methods?

What are your thoughts on documenting these methods? Do something now or kick the can down the road until we have legit API reference docs for this client?

Add tests for calling async_run concurrently

Signed-off-by: Mattt Zmuda <mattt@replicate.com>
@mattt
Copy link
Contributor Author

mattt commented Nov 8, 2023

@zeke Thanks for the review!

Wow this looks like it was a lot of work.

It sure was 🫠

async variants with the async_ prefix

Is this a cow-path convention in Python land for async variants of synchronous methods?

As I understand it, yes. Another convention I've seen is prepending just a (so aget instead of async_get), but I think you'll agree with my choice here.

The other (anti) pattern I've seen is to make it a separate package / module. So long as sync and async can coexist in replicate without causing problems, then I'm keen to keep them together.

What are your thoughts on documenting these methods? Do something now or kick the can down the road until we have legit API reference docs for this client?

I added a callout towards the top of the README about methods having async variants. As for API reference documentation, the docstrings in the code itself are in good shape. Some Python projects I like use mkdocstrings to generate those automatically. So that's something to consider 1.

Footnotes

  1. I'm a fan of publishing docs where people expect to see them. For our Swift library, I recently set up docs hosted on Swift Package Index. For Go, I recently linked to pkg.go.dev.

mattt added 3 commits November 8, 2023 14:47
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
Signed-off-by: Mattt Zmuda <mattt@replicate.com>
@mattt mattt merged commit 6ed3cf7 into main Nov 9, 2023
@mattt mattt deleted the mattt/async branch November 9, 2023 11:01
@mattt mattt mentioned this pull request Nov 9, 2023
@antont
Copy link

antont commented Nov 9, 2023

The other (anti) pattern I've seen is to make it a separate package / module. So long as sync and async can coexist in replicate without causing problems, then I'm keen to keep them together.

+1

Some libs use an unasync tool to only maintain the async code, and generate the sync code from it using a script. For example https://github.com/ioxiocom/firedantic/blob/main/README.md#about-sync-and-async-versions-of-library documents their use which is implemented in https://github.com/ioxiocom/firedantic/blob/main/unasync.py

@mattt
Copy link
Contributor Author

mattt commented Nov 9, 2023

@antont Interesting! Thank you for sharing that. The code base is at a manageable size now so that doing things manually isn't onerous, but I'll definitely want to set up automation like this once things get out of hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error: "Connection pool is full" Is there an async option while calling predict in the client? Request: Add async support
3 participants