In the previous post I explained how hardware optimized images are used to get the best performance / functionality out of a node. ReCap Running binaries only compiled for generic x86-64, does not give you all the nice CPU flags: $ docker run --rm -ti --device=/dev/nvidia{0,ctl,-uwm} qnib/cv-tf-dev:1.12.0 Using TensorFlow backend. Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA Compiled for a Broadwell CPU does, but the image used here includes the CUDA toolkit for CUDA9.0, while the host provides the CUDA driver for CUDA 9.2: $ docker run --rm -ti --device=/dev/nvidia{0,ctl,-uwm} qnib/cv-nccl90-tf-dev:broadwell_1.12.0 Using TensorFlow backend. libcuda reported version is: 390.30.0 kernel reported version is: 396.44.0 kernel version 396.44.0 does not match DSO version 390.30.0 -- cannot find working devices in this configuration [] An image build with CUDA 9.2 gets us one step closer. Unfortunately TensorFlow is compiled with default flags which requires the latest GPUs (>NVIDIA P100). $ docker run --rm -ti --device=/dev/nvidia{0,ctl,-uwm} qnib/cv-nccl92-tf-dev:broadwell_1.12.0 Using TensorFlow backend. Ignoring visible gpu device (device: 0, name: Tesla M60, pci bus id: 0000:00:1e.0, compute capability: 5.2) with Cuda compute capability 5.2. The minimum required Cuda capability is 7.0. [] What is needed is an image compiled with CUDA_COMPUTE_CAPABILITIES=5.2. $ docker run --rm -ti --device=/dev/nvidia{0,ctl,-uwm} qnib/cv-nccl92-tf-dev:broadwell_nvcap52_1.12.0 Using TensorFlow backend. Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 6723 MB memory) -> physical GPU (device: 0, name: Tesla M60, pci bus id: 0000:00:1e.0, compute capability: 5.2) ['/job:localhost/replica:0/task:0/device:GPU:0'] Naming Sucks That was possible for a long time now and it did not fly because incorporating the target into image names and tags just plain sucks. It does not really help when submitting a job that get scheduled on a Broadwell with M60 OR a Skylake with two V100s. For this to work one needs to know in advance where it is going to be scheduled and thus the scheduling needs to be constraint, so that the workload only gets scheduled on a node that matches the image. Platform FTW That is not the first time that problem was solved tho. Official base images designed to run on multiple platforms will work it out through ManifestLists. A ManifestList is just an index of images identified by a platform object. The tool manifest-tool (and docker manifest btw) allows to specify this using a simple yaml file: image: myprivreg:5000/someimage:latest manifests: - image: myprivreg:5000/someimage:ppc64le platform: architecture: ppc64le os: linux - image: myprivreg:5000/someimage:amd64 platform: architecture: amd64 os: linux In order to use it docker pull has an experimental feature --platform. Downloading the PowerPC version of ubuntu? Just do: $ docker pull --platform=linux/ppc64le ubuntu Using default tag: latest latest: Pulling from library/ubuntu 2a9179d9b269: Pull complete 8fe609a92e3f: Pull complete b726957e1026: Pull complete 42ba7c91fb87: Pull complete Digest: sha256:7a47ccc3bbe8a451b500d2b53104868b46d60ee8f5b35a24b41a86077c650210 Status: Downloaded newer image for ubuntu:latest Granted, that does not make much sense in the context of CPU architectures, as you won't be able to run this image on AMD64. $ docker run -ti --rm ubuntu echo Huhu standard_init_linux.go:207: exec user process caused "exec format error" $ docker pull --platform=linux/amd64 ubuntu Using default tag: latest latest: Pulling from library/ubuntu Digest: sha256:7a47ccc3bbe8a451b500d2b53104868b46d60ee8f5b35a24b41a86077c650210 Status: Downloaded newer image for ubuntu:latest $ docker run -ti --rm ubuntu echo Huhu Huhu But still... :) Platform Applied In the context of what is discussed here, I incorperate the different aspects of the images into one 'meta' image (a.k.a ManifestList). I compacted the yaml a bit to not use to much space. image: qnib/cv-tf:1.12.0-rev9 manifests: image: qnib/cv-tf-dev:1.12.0-rev11 platform: architecture: amd64 os: linux image: qnib/cv-tf-dev:skylake_1.12.0-rev6 platform: features: - skylake image: qnib/cv-nccl90-tf-dev:1.12.0-rev1 platform: features: - nvidia-390-30 image: qnib/cv-nccl92-tf-dev:1.12.0-rev11 platform: features: - nvidia-396-44 image: qnib/cv-nccl90-tf-dev:broadwell_1.12.0-rev2 platform: features: - broadwell - nvidia-390-30 image: qnib/cv-nccl92-tf-dev:broadwell_1.12.0-rev8 platform: features: - broadwell - nvidia-396-44 image: qnib/cv-nccl92-tf-dev:skylake_1.12.0-rev6 platform: features: - nvidia-396-44 - skylake image: qnib/cv-nccl92-tf-dev:skylake512_1.12.0-rev8 platform: features: - nvidia-396-44 - skylake512 image: qnib/cv-nccl92-tf-dev:nvcap52_1.12.0-rev3 platform: features: - nv-compute-5-2 - nvidia-396-44 image: qnib/cv-nccl92-tf-dev:nvcap37_1.12.0-rev4 platform: features: - nv-compute-3-7 - nvidia-396-44 image: qnib/cv-nccl92-tf-dev:broadwell_nvcap52_1.12.0-rev2 platform: features: - broadwell - nv-compute-5-2 - nvidia-396-44 This ManifestList can now be used to download the correct image via --platform (with a little change to the engine): $ docker pull --platform=linux/amd64:broadwell:nv-compute-5-2:nvidia-396-44 qnib/cv-tf:1.12.0-rev9 1.12.0-rev9: Pulling from qnib/cv-tf Digest: sha256:bb3ffb86b26892c03667544a7ec296ea0f8bc76842adb4d702bf32baacdc0221 Status: Downloaded newer image for qnib/cv-tf:1.12.0-rev9 This results in the same image as before. $ docker image inspect -f '{{.Id}}' qnib/cv-nccl92-tf-dev:broadwell_nvcap52_1.12.0-rev2 sha256:21894c739c326d6f3942dfcf36cb9afb73f951f9aafab6b049d273323a0429e8 $ docker image inspect -f '{{.Id}}' qnib/cv-tf:1.12.0-rev9 sha256:21894c739c326d6f3942dfcf36cb9afb73f951f9aafab6b049d273323a0429e8 Engine Configuration In order to be practical, this needs to be configured on an engine level, so that my Tensorflow job specifies the generic name qnib/cv-tf:1.12.0-rev9 and the engine will download the correct image for the system it runs on. One possible idea is to put it in the daemon.json, like this: $ sudo cat /etc/docker/daemon.json { "debug": true, "tls": true, "tlscacert": "/etc/docker/ca.pem", "tlscert": "/etc/docker/cert.pem", "tlskey": "/etc/docker/key.pem", "tlsverify": true, "experimental": true, "platform-features": [ "broadwell", "nv-compute-5-2", "nvidia-396-44" ] } Doing so, the engine will add the platform-features automatically, quite like the manual download using: --platform=linux/amd64:broadwell:nv-compute-5-2:nvidia-396-44. No need to specify different image names to make sure the correct image is scheduled. The following K8s job will fetch the correct image depending on the engine it is scheduled on. apiVersion: batch/v1 kind: Job metadata: name: TensorFlow spec: backoffLimit: 1 template: spec: containers: - name: tensorflow image: qnib/cv-tf:1.12.0-rev9 resources: limits: qnib.org/gpu: 1 Ongoing Discussions IMHO that is going to improve the reproducible, deterministic, reliable execution of images with a dependency on the host. Be it a GPU or a CPU. If you want to know more, visit the moby/moby issue and add a :thumbsup: to show your support. :) Next The next blog post will show how I build this using CI/CD and stay sane.