# Platform Quickstart This guide provides a detailed description of using the [Platform SDK](https://github.com/voxel51/platform-sdk) to wrap your custom analytic for deployment to the [Voxel51 Platform](https://console.voxel51.com). See the [examples folder](https://github.com/voxel51/platform-sdk/tree/develop/examples) for pre-defined examples of analytics that you can build and deploy to the Platform to get comfortable with the workflow. voxel51-logo.png ## Docker installation All analytics are deployed to the Voxel51 Platform as [Docker images](https://www.docker.com). If you are new to Docker, we recommend that you: - Install the Community Edition (CE) on your machine by following the simple instructions at https://docs.docker.com/install - Read the Docker orientation guide at https://docs.docker.com/get-started to get familar with the concepts ## Analytic executable The following code provides an annotated example of a generic Python executable `main.py` that acts as the main entrypoint for an analytic Docker image. It uses the Platform SDK to: - Parse the task description provided to the image at runtime - Download the inputs and parameters for the task - Report the necessary metadata to the platform - Invoke the analytic-specific implementation - Publish the outputs of the task to the platform - Mark the task as complete It also demonstrates how to appropriately handle runtime errors that may occur during execution. You can easily adapt this template to run your custom algorithm by inserting the appropriate calls in the `Your code here!` section. ```python #!/usr/bin/env python ''' Template main executable for a Voxel51 Platform Analytic. ''' import logging # The `platform-sdk` package must be pip installed in your image import voxel51.platform.task as voxt # # The following constants set paths in the internal file system of your Docker # image to which you want to download task input(s), write outputs, etc. # # # A path to write the logfile for this task. # # Don't change this path; the platform attaches a pre-stop hook to all images # that will upload any logfile from this location whenever a task is # terminated unexpectedly (e.g., preemption, resource violation, etc.) # TASK_LOGFILE_PATH = "/var/log/image.log" # # A directory to which to download the task input(s) for your task. # This location can be anything you want. # INPUTS_DIR = "/path/to/inputs" # # The local path to which your analytic will write it's final output. The file # type you specify here depends on the nature of your analytic. This location # can be anything you want. # OUTPUT_PATH = "/path/to/output.json" logger = logging.getLogger(__name__) def main(): # # Setup logging # # This command configures system-wide logging so that all logging recorded # via the builtin `logging` module will be written to the logfile path that # you provide here. Note that the platform stores task logs internally to # facilitate debugging, but these logs are not made available to end-users. # voxt.setup_logging(TASK_LOGFILE_PATH) # # Get task config URL # # When a platform Docker image is executed, the `TASK_DESCRIPTION` # environment variable is set to a signed URL at which the TaskConfig for # the task can be downloaded. # task_config_url = voxt.get_task_config_url() try: # # Create a TaskManager for the task # # The TaskManager class provides a convenient interface to read inputs # and parameters for your task, publish its status during execution, # and upload the output when the task completes. # # This command downloads the TaskConfig from `task_config_url` and # stores a TaskStatus instance internally to record the status of the # task. # task_manager = voxt.TaskManager.from_url(task_config_url) except: # # Something went terribly wrong and we are unable to communicate with # the platform. This command logs the failure and notifies the platform # as fully as possible. # voxt.fail_epically(logfile_path=TASK_LOGFILE_PATH) return try: # # Mark the task as started # # This command updates the state of the TaskStatus stored in the # TaskManager and then publishes the status to the platform. # task_manager.start() # # Download inputs # # This command downloads the inputs for your task to the directory that # you specify here. It returns a dictionary mapping the input names to # the paths of each downloaded file in the directory. The inputs are # downloaded from the signed URLs contained in the TaskConfig provided # to your Docker image when it was run. The names of the inputs are # configured by the analytic JSON that you provided when publishing the # analytic to the platform. # inputs = task_manager.download_inputs(INPUTS_DIR) # # Parse parameters # # This command parses any parameters provided in the TaskConfig for the # task. It returns a dictionary that maps parameter names to their # corresponding values. The names of the parameters are configured by # the analytic JSON that you provided when publishing the analytic to # the platform. # parameters = task_manager.parse_parameters() # # Record metadata and post job metadata # # The code below performs two tasks: it records the metadata about the # task inputs, and it publishes the metadata about the overall task to # the platform. # # The input metadata is required by the platform in order to, for # example, know the frame rate of the input video when rendering the # annotations generated by an object detection analytic. # # Posting metadata about the job to the platform is required so that # the platform can track the data volume processed by the task. # # The code below assumes the typical case where the analytic has only # one input, and that input is a video. If your analytic does not take # a single video as input, then you can comment out the lines below; # note, however, that the details of the data processed by your job # will not be fully tracked by the platform, and thus certain features # like output preview and data volume reporting will not be supported. # input_name = list(inputs.keys())[0] input_path = inputs[input_name] task_manager.record_input_metadata(input_name, video_path=input_path) task_manager.post_job_metadata(video_path=input_path) # # Your code goes here! # # Now you have the inputs and parameters required to run your task, so # you can perform your custom work! # # Remember that any `logging` messages you generated will be # automatically recorded in your task's logfile. The TaskManager can # also store messages, which are included in the status JSON file made # available to end users. You can publish the latest status of your # task to the platform at any time by calling `publish_status()`. # logger.info("Logging messages will appear in the task's logfile") task_manager.add_status_message("TaskManager can track messages") task_manager.publish_status() # # Upload output # # This command uploads the output that you generated at the provided # path to the platform. # task_manager.upload_output(OUTPUT_PATH) # # Mark task as complete # # This command marks the task as complete and publishes the final task # status and logfile to the platform. You are done! # task_manager.complete(logfile_path=TASK_LOGFILE_PATH) except: # # An error occured while your analytic was executing, so the command # below gracefully exits by recording the stack trace in your logfile, # marking the task as failed, and posting the final (failed) task # status to the platform. # # # Note that the `fail_gracefully` function allows you to specify a # failure type for the job from the TaskFailureType enum. Here we # assume that the platform and analytic are working perfectly and any # error must have resulted from invalid user data. However, the failure # type can be customized depending on the nature of the error. # task_manager.fail_gracefully( failure_type=voxt.TaskFailureType.USER, logfile_path=TASK_LOGFILE_PATH) if __name__ == "__main__": main() ``` ## Docker entrypoint In order to gracefully handle any uncaught exceptions in the analytic executable defined above, we strongly recommend wrapping your executable in the following simple `main.bash` script, which will act as the entrypoint to your Docker image. ```shell #!/bin/bash # Main entrypoint for a Voxel51 Platform Analytic. # # # Don't change `LOGFILE_PATH`. The platform attaches a pre-stop hook to images # at runtime that will upload the logfile from this location whenever a task is # terminated unexpectedly (e.g., preemption, resource violation, etc.) # LOGFILE_PATH=/var/log/image.log BACKUP_LOGFILE_PATH=/var/log/backup.log # # Execute analytic and pipe stdout/stderr to disk so that this information # will be available in case of errors. # # If necessary, replace `python main.py` here with the appropriate invocation # for your analytic. # set -o pipefail python main.py 2>&1 | tee "${BACKUP_LOGFILE_PATH}" # Gracefully handle uncaught failures in analytic if [ $? -ne 0 ]; then # Append backlog log echo "UNCAUGHT EXCEPTION; APPENDING BACKUP LOG" >> "${LOGFILE_PATH}" cat "${BACKUP_LOGFILE_PATH}" >> "${LOGFILE_PATH}" # Post job failure curl -X PUT "${API_BASE_URL}/jobs/${JOB_ID}/state" \ -H "X-Voxel51-Agent: ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"state": "FAILED", "failure_type": "ANALYTIC"}' # Upload logfile curl -T "${LOGFILE_PATH}" -X PUT "${LOGFILE_SIGNED_URL}" fi ``` The script simply executes the main executable from the previous section, pipes its `stdout` and `stderr` to disk, and, if the executable exits with a non-zero status code, marks the job as `FAILED` and uploads the logfile to the platform. This extra layer of protection is important to catch and appropriately report errors that prevent the Platform SDK from loading (e.g., an `import` error caused from buggy installation instructions in the `Dockerfile`). ## Docker build This section assumes that you have populated the analytic executable and entrypoint scripts from the previous sections to run your custom analytic. The snippet below defines a `Dockerfile` that installs the Platform SDK and its dependencies in a GPU-enabled Docker image that runs the `main.bash` and `main.py` scripts that you provide. It can be easily extended to include any custom installation requirements for your analytic. ```shell # A typical base image for GPU deployments. Others are possible FROM nvidia/cuda:9.0-cudnn7-runtime-ubuntu16.04 # # Install `platform-sdk` and its dependencies # # The Platform SDK supports either Python 2.7.X or Python 3.6.X # # For CPU-enabled images, install tensorflow==1.12.0 # # For GPU-enabled images, use the TensorFlow version compatible with the CUDA # version in your image: # - CUDA 8: tensorflow-gpu==1.4.0 # - CUDA 9: tensorflow-gpu==1.12.0 # - CUDA 10: tensorflow-gpu==1.14.0 # # The following installs Python 3.6 with TensorFlow 1.12.0 to suit the base # NVIDIA image chosen above. # COPY platform-sdk/ /engine/platform-sdk/ RUN apt-get update \ && apt-get -y --no-install-recommends install \ software-properties-common \ && add-apt-repository -y ppa:deadsnakes/ppa \ && apt-get update \ && apt-get -y --no-install-recommends install \ sudo \ build-essential \ pkg-config \ ca-certificates \ unzip \ git \ curl \ libcupti-dev \ python3.6 \ python3-dev \ python3-pip \ python3-setuptools \ ffmpeg \ imagemagick \ && ln -s /usr/bin/python3 /usr/bin/python \ && ln -s /usr/bin/pip3 /usr/bin/pip \ && python -m pip install --upgrade pip \ && python -m pip --no-cache-dir install -r /engine/platform-sdk/requirements.txt \ && python -m pip --no-cache-dir install -r /engine/platform-sdk/eta/requirements.txt \ && python -m pip --no-cache-dir install -e /engine/platform-sdk/. \ && python -m pip --no-cache-dir install -e /engine/platform-sdk/eta/. \ && python -m pip --no-cache-dir install -I tensorflow-gpu==1.12.0 \ && python -m pip --no-cache-dir install --upgrade numpy==1.16.0 \ && rm -rf /var/lib/apt # # Your custom installation here! # # Expose port so image can read/write from external storage at runtime EXPOSE 8000 # Setup entrypoint COPY main.bash /engine/main.bash COPY main.py /engine/main.py RUN mkdir -p /var/log WORKDIR /engine ENTRYPOINT ["bash", "main.bash"] ``` You can build your image from the above `Dockerfile` by running: ```shell # Clone platform-sdk git clone https://github.com/voxel51/platform-sdk cd platform-sdk git submodule init git submodule update cd .. # # Your custom setup here! # # Build image docker build -t "" . ``` ## Local testing After you have built the Docker image for your custom analytic, you can use the `test-platform` script that was installed along with the Platform SDK to verify that your image is functioning properly before deploying it to the Voxel51 Platform. The basic syntax for launching a test server instance is: ```shell test-platform \ --analytic-image \ --analytic-json \ --inputs = \ --compute-type ``` Type `test-platform -h` for help, and see the [this folder](https://github.com/voxel51/platform-sdk/tree/develop/tests/platform) for more informtion. ## Docker deployment Once your Docker image is ready for deployment, you must save it as a `.tar.gz` file so that you can upload it to the Voxel51 Platform. To do so, simply execute a command like: ```shell docker save | gzip -c > .tar.gz ``` Finally, follow the instructions in the [Analytic deployment section of the README](https://github.com/voxel51/platform-sdk/blob/develop/README.md#analytic-deployment) to publish your analytic to the platform. ## Copyright Copyright 2017-2020, Voxel51, Inc.
[voxel51.com](https://voxel51.com)