diff --git a/examples/runtime/hello_world/README.md b/examples/runtime/hello_world/README.md new file mode 100644 index 0000000000..e9323fe6b7 --- /dev/null +++ b/examples/runtime/hello_world/README.md @@ -0,0 +1,119 @@ + + +# Hello World Example + +This is the simplest Dynamo example demonstrating a basic service using Dynamo's distributed runtime. It showcases the fundamental concepts of creating endpoints and workers in the Dynamo runtime system. + +## Architecture + +```text +Client (dynamo_worker) + │ + ▼ +┌─────────────┐ +│ Backend │ Dynamo endpoint (/generate) +└─────────────┘ +``` + +## Components + +- **Backend**: A Dynamo service with an endpoint that receives text input and streams back greetings for each comma-separated word +- **Client**: A Dynamo worker that connects to and sends requests to the backend service, then prints out the response + +## Implementation Details + +The example demonstrates: + +- **Endpoint Definition**: Using the `@dynamo_endpoint` decorator to create streaming endpoints +- **Worker Setup**: Using the `@dynamo_worker()` decorator to create distributed runtime workers +- **Service Creation**: Creating services and endpoints using the distributed runtime API +- **Streaming Responses**: Yielding data for real-time streaming +- **Client Integration**: Connecting to services and processing streams +- **Logging**: Basic logging configuration with `configure_dynamo_logging` + +## Getting Started + +## Prerequisites + + Before running this example, ensure you have the following services running: + + - **etcd**: A distributed key-value store used for service discovery and metadata storage + - **NATS**: A high-performance message broker for inter-component communication + + You can start these services using Docker Compose: + + ```bash + # clone the dynamo repository if necessary + # git clone https://github.com/ai-dynamo/dynamo.git + cd dynamo + docker compose -f deploy/metrics/docker-compose.yml up -d + ``` + +### Running the Example + +First, start the backend service: +```bash +cd examples/runtime/hello_world +python hello_world.py +``` + +Second, in a separate terminal, run the client: +```bash +cd examples/runtime/hello_world +python client.py +``` + +The client will connect to the backend service and print the streaming results. + +### Expected Output + +When running the client, you should see streaming output like: +```text +Hello world! +Hello sun! +Hello moon! +Hello star! +``` + +## Code Structure + +### Backend Service (`hello_world.py`) + +- **`content_generator`**: A dynamo endpoint that processes text input and yields greetings +- **`worker`**: A dynamo worker that sets up the service, creates the endpoint, and serves it + +### Client (`client.py`) + +- **`worker`**: A dynamo worker that connects to the backend service and processes the streaming response + +## Deployment to Kubernetes. + +Follow the [Quickstart Guide](../../../docs/guides/dynamo_deploy/quickstart.md) to install Dynamo Cloud. +Then deploy to kubernetes using + +```bash +export NAMESPACE= +cd dynamo +kubectl apply -f examples/runtime/hello_world/deploy/hello_world.yaml -n ${NAMESPACE} +``` + +to delete your deployment issue: + +```bash +kubectl delete dynamographdeployment hello-world -n ${NAMESPACE} +``` \ No newline at end of file diff --git a/examples/runtime/hello_world/client.py b/examples/runtime/hello_world/client.py new file mode 100644 index 0000000000..c685dff407 --- /dev/null +++ b/examples/runtime/hello_world/client.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +import uvloop + +from dynamo.runtime import DistributedRuntime, dynamo_worker + + +@dynamo_worker() +async def worker(runtime: DistributedRuntime): + # Get endpoint + endpoint = ( + runtime.namespace("hello_world").component("backend").endpoint("generate") + ) + + # Create client and wait for service to be ready + client = await endpoint.client() + await client.wait_for_instances() + + # Issue request and process the stream + stream = await client.generate("world,sun,moon,star") + async for response in stream: + print(response.data()) + + +if __name__ == "__main__": + uvloop.install() + asyncio.run(worker()) diff --git a/examples/runtime/hello_world/deploy/hello_world.yaml b/examples/runtime/hello_world/deploy/hello_world.yaml new file mode 100644 index 0000000000..0c92ff0062 --- /dev/null +++ b/examples/runtime/hello_world/deploy/hello_world.yaml @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: hello-world +spec: + services: + Frontend: + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 60 + periodSeconds: 60 + timeoutSeconds: 30 + failureThreshold: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'echo ok' + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 10 + dynamoNamespace: hello-world + componentType: main + replicas: 1 + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "1" + memory: "2Gi" + extraPodSpec: + mainContainer: + image: gitlab-master.nvidia.com:5005/dl/ai-dynamo/dynamo/dynamo:helloworld + workingDir: /workspace/examples/runtime/hello_world/ + command: + - /bin/sh + - -c + args: + - "python3 client.py" + HelloWorldWorker: + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "exit 0" + periodSeconds: 60 + timeoutSeconds: 30 + failureThreshold: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'grep "Serving endpoint" /tmp/hello_world.log' + initialDelaySeconds: 60 + periodSeconds: 60 + timeoutSeconds: 30 + failureThreshold: 10 + dynamoNamespace: hello-world + componentType: worker + replicas: 1 + resources: + requests: + cpu: "10" + memory: "20Gi" + gpu: "1" + limits: + cpu: "10" + memory: "20Gi" + gpu: "1" + extraPodSpec: + mainContainer: + image: gitlab-master.nvidia.com:5005/dl/ai-dynamo/dynamo/dynamo:helloworld + workingDir: /workspace/examples/runtime/hello_world/ + command: + - /bin/sh + - -c + args: + - python3 hello_world.py 2>&1 | tee /tmp/hello_world.log diff --git a/examples/runtime/hello_world/hello_world.py b/examples/runtime/hello_world/hello_world.py new file mode 100644 index 0000000000..2fbe2b4596 --- /dev/null +++ b/examples/runtime/hello_world/hello_world.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import logging + +import uvloop + +from dynamo.runtime import DistributedRuntime, dynamo_endpoint, dynamo_worker +from dynamo.runtime.logging import configure_dynamo_logging + +logger = logging.getLogger(__name__) +configure_dynamo_logging(service_name="backend") + + +@dynamo_endpoint(str, str) +async def content_generator(request: str): + logger.info(f"Received request: {request}") + for word in request.split(","): + await asyncio.sleep(1) + yield f"Hello {word}!" + + +@dynamo_worker() +async def worker(runtime: DistributedRuntime): + namespace_name = "hello_world" + component_name = "backend" + endpoint_name = "generate" + lease_id = runtime.etcd_client().primary_lease_id() + + component = runtime.namespace(namespace_name).component(component_name) + await component.create_service() + + logger.info(f"Created service {namespace_name}/{component_name}") + + endpoint = component.endpoint(endpoint_name) + + logger.info(f"Serving endpoint {endpoint_name} on lease {lease_id}") + await endpoint.serve_endpoint(content_generator) + + +if __name__ == "__main__": + uvloop.install() + asyncio.run(worker())