Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
# 🚀 {{ object.title }} Template
# {{ object.title }} Template

Thank you for hosting your algorithm on Grand-Challenge.org, we appreciate it!
Thank you for hosting your algorithm on grand-challenge.org. We appreciate it!
This algorithm template provides a tailored example to help you run [your algorithm on
grand-challenge.org]({{ object.url }}).

## Content
It contains:

This Algorithm Template contains a tailored example to help you run [your
algorithm on Grand-Challenge.org]({{ object.url }}).
* A set of example Bash scripts (.sh) to help you test your algorithm locally and export it as a container for upload.
* An example Python implementation that demonstrates how to run the algorithm inference server (`app.py`) and how to
read inputs and write outputs (`inference.py`).

It contains the following:
* ️🦾 A set of example bash scripts (.sh) to _help_ you test your algorithm locally and export it as a container for upload.
* 🦿 An example _python implementation_ that templates reading the inputs and writing the outputs.

See the header of the `inference.py` for more information.

Please note that this is supplementary to the [documentation](https://grand-challenge.org/documentation/algorithms/).
If the documentation does not answer your question, feel free to reach out to us at
[support@grand-challenge.org](mailto:support@grandchallenge.org).

The examples in this template illustrate one approach, but they are not the only option. Any container image will do as long as it:
This template is provided purely as an example of one possible workflow. Any container image is acceptable as long as
it behaves as expected and interacts with the platform through the defined input and output paths.

{% for interface in object.algorithm_interfaces %}
{% if not loop.first %}or input/outputs as follows:{% endif %}
{% if not forloop.first %}or uses the following inputs and outputs:{% endif %}
{% for input in interface.inputs %}
- reads from the `/input/{{ input.relative_path}}`
- Reads input from `/input/{{ input.relative_path}}`
{% endfor %}
{% for output in interface.outputs %}
- outputs, correctly, to `/output/{{ output.relative_path }}`
- Writes output to `/output/{{ output.relative_path }}`
{% endfor %}
{% endfor %}

## Running the container locally

To run the container locally, execute:

./do_test_run.sh

This script will:

1. Start the inference server and load your model
2. Wait until the server becomes healthy (i.e. the health endpoint returns HTTP 200 OK)
3. Invoke the algorithm for inference and wait for it to complete (i.e. the invoke endpoint returns HTTP 201 CREATED)

During inference, the container reads input data from:

`./test/input`

and writes output results to:

`./test/output`

## Saving the container

To save the container and prepare it for upload to grand-challenge.org, run:

./do_save.sh

## Further documentation

Please note that this is supplementary to the [documentation](https://grand-challenge.org/documentation/algorithms/).

For a step-by-step tutorial, see:
https://grand-challenge.org/documentation/building-and-testing-the-container/

For details about the runtime environment used on the platform, see:
https://grand-challenge.org/documentation/runtime-environment/

If the documentation does not answer your question, feel free to reach out to us at
[support@grand-challenge.org](mailto:support@grand-challenge.org).

---
Generated by [Grand-Challenge-Forge](https://github.com/DIAGNijmegen/rse-grand-challenge-forge) v{{ grand_challenge_forge_version }}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ RUN python -m pip install \
--no-color \
--requirement /opt/app/requirements.txt

COPY --chown=user:user app.py /opt/app/
COPY --chown=user:user inference.py /opt/app/

ENTRYPOINT ["python", "inference.py"]
LABEL org.grand-challenge.api-method="invoke"

ENTRYPOINT ["python", "app.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{% load forge %}
"""
The following is an example algorithm inference server.

Any implementation will do as long as it:

1. Starts the inference server and loads the algorithm
2. On the health endpoint indicates if the server is healthy (i.e. returns HTTP 200 OK)
3. On the invoke endpoint invokes the algorithm for inference and returns HTTP 201 CREATED

"""
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, Response, status
import torch
import uvicorn

import inference


from uvicorn.config import LOGGING_CONFIG


def _show_torch_cuda_info():
print("=+=" * 10)
print("Collecting Torch CUDA information")
print(f"Torch CUDA is available: {(available := torch.cuda.is_available())}")
if available:
print(f"\tnumber of devices: {torch.cuda.device_count()}")
print(f"\tcurrent device: { (current_device := torch.cuda.current_device())}")
print(f"\tproperties: {torch.cuda.get_device_properties(current_device)}")
print("=+=" * 10)


def init_model():
# Initialize your model: any way you'd like, here we show-case torch
_show_torch_cuda_info()

# Example how to set torch to use the GPU (if available)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model = torch.nn.Linear(10, 1).to(device)

# Your model will be extracted to the `model_dir` at runtime on Grand Challenge
# Note: when testing locally, the local `./model` directory is mounted here.
# Eventually, you should upload it as a tarball to Grand Challenge!
# Go to Algorithm and upload it under Models.
model_dir = Path("/opt/ml/model")
with open(
model_dir / "a_tarball_subdirectory" / "some_tarball_resource.txt", "r"
) as f:
print(f.read())

return model


MODELS = {}


# During the lifespan of your inference server, your model should be ready
# for invocations.It is important to load your model here, and not just
# before running inference, to allow the inference time to be as short as
# possible. Each invocation will have a timeout, so if your model still
# needs to be loaded when the /invoke endpoint is called, there may not be
# enough time for processing.
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load the ML model
MODELS["answer_to_everything"] = init_model()
yield
# Clean up the models and release the resources
MODELS.clear()


app = FastAPI(lifespan=lifespan)


# After starting your inference server, the health endpoint will
# be called repeatedly until it returns a 200 response.
# Redirect responses will not be followed and will raise an exception.
# Any other response will be ignored.
@app.get("/health")
async def health():
try:
# check if the model is initialized
_ = MODELS["answer_to_everything"]
return Response(status_code=status.HTTP_200_OK)
except KeyError:
return Response(status_code=status.HTTP_404_NOT_FOUND)


# After the health endpoint returns a 200 response,
# the invoke endpoint will be called (one or more times)
# to invoke inference on the inputs in the input folder.
# When inference is done, this endpoint should return a 201 response.
# Any other response will raise an exception and fail.
@app.post("/invoke")
async def invoke():
model = MODELS["answer_to_everything"]
inference.run(model)
return Response(status_code=status.HTTP_201_CREATED)


if __name__ == "__main__":
log_config = LOGGING_CONFIG.copy()
log_config["handlers"]["default"]["stream"] = "ext://sys.stdout"
uvicorn.run(app, host="0.0.0.0", port=4743, log_config=log_config)
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ echo "==== Done"
echo ""


# Create the tarbal
# Create the tarball
echo "= STEP 3 = Packing the model"
echo "This can take a while."
output_tarball_name="${SCRIPT_DIR}/model.tar.gz"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ set -e

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
DOCKER_IMAGE_TAG="example_algorithm_{{ object.slug }}"
CONTAINER_NAME="example_algorithm_{{ object.slug }}_container"
PORT=37847

DOCKER_NOOP_VOLUME="${DOCKER_IMAGE_TAG}-volume"

Expand All @@ -26,6 +28,9 @@ cleanup() {
$DOCKER_IMAGE_TAG \
-c "chmod -R -f o+rwX /output/* || true"

docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
echo "=+= Container stopped"

# Ensure volume is removed
docker volume rm "$DOCKER_NOOP_VOLUME" > /dev/null
}
Expand Down Expand Up @@ -56,12 +61,14 @@ docker volume create "$DOCKER_NOOP_VOLUME" > /dev/null

trap cleanup EXIT

run_docker_forward_pass() {
start_docker_container() {
local interface_dir="$1"

echo "=+= Doing a forward pass on ${interface_dir}"
echo "=+= Starting container"

## Note the extra arguments that are passed here:
# '-p ${PORT}:4743'
# maps local port to container port 4743
# '--network none'
# entails there is no internet connection
# '--gpus all'
Expand All @@ -70,20 +77,83 @@ run_docker_forward_pass() {
# is added because on Grand Challenge this directory cannot be used to store permanent files
# '--volume ../model:/opt/ml/model/":ro'
# is added to provide access to the (optional) tarball-upload locally
docker run --rm {% if not no_gpus %}--gpus all {% endif %}\
docker run -d {% if not no_gpus %}--gpus all {% endif %}\
--name "$CONTAINER_NAME" \
--platform=linux/amd64 \
--network none \
-p ${PORT}:4743 \
--volume "${INPUT_DIR}/${interface_dir}":/input:ro \
--volume "${OUTPUT_DIR}/${interface_dir}":/output \
--volume "$DOCKER_NOOP_VOLUME":/tmp \
--volume "${SCRIPT_DIR}/model":/opt/ml/model:ro \
"$DOCKER_IMAGE_TAG"
"$DOCKER_IMAGE_TAG" \
>/dev/null

echo "=+= Container started"
}

check_health() {
echo "=+= Waiting for health endpoint..."

local max_attempts=30
local delay=10

for ((i=1;i<=max_attempts;i++)); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 10 \
http://localhost:${PORT}/health || echo "000")

echo "Health check attempt $i/$max_attempts returned $STATUS"

if [[ "$STATUS" == "200" ]]; then
echo "=+= API healthy"
return 0
fi

if [[ "$STATUS" == "302" ]]; then
echo "Health endpoint returned 302 — failing"
return 1
fi

echo "Retrying in ${delay}s"
sleep "$delay"
done

echo "Health endpoint never returned 200"
return 1
}

run_docker_forward_pass() {
local interface_dir="$1"

echo "=+= Doing a forward pass on ${interface_dir}"

echo "=+= Calling invoke endpoint"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 300 \
-X POST http://localhost:${PORT}/invoke || echo "000")

if [ "$STATUS" != "201" ]; then
echo "Invoke failed with status $STATUS"
exit 1
fi

echo "=+= Invoke completed"

echo "=+= Wrote results to ${OUTPUT_DIR}/${interface_dir}"
}

echo "=+= Wrote results to ${OUTPUT_DIR}/${interface_dir}"
stop_docker_container() {
echo "=+= Stopping container"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
echo "=+= Container stopped"
}

{% for interface_name in object.algorithm_interface_names %}
start_docker_container "{{ interface_name }}"
check_health
run_docker_forward_pass "{{ interface_name }}"
stop_docker_container
{% endfor %}


Expand Down
Loading
Loading