Đã bao giờ bạn gặp tình huống:
Tại sao code trên máy tính của tôi chạy mà mang sang máy tính của bạn lại không chạy?
99% câu trả lời cho câu hỏi này là do sự khác biệt về môi trường giữa 2 máy tính, 1% còn lại là do các nguyên nhân khác như copy thiếu file, sai đường dẫn, sử dụng câu lệnh không đúng, …
Trong bài toán AI, tình trạng này lại càng phổ biến hơn, bởi vì một model AI yêu cầu cơ man nào là thư viện đi kèm, thư viện này liên kết, ràng buộc với thư viện kia. Không những thế, với tốc độ phát triển như vũ bão hiện nay của AI, các thư viện cũng liên tục cập nhật phiên bản mới, và có khi code sử dụng phiên bản cũ lại không chạy được trên phiên bản mới. Rồi thì thư viện A phiên bản 1.x lại chỉ tương thích với thư viện B phiên bản 1.x.x, nếu ta cứ nhắm mắt gõ pip install abc
thì mặc định sẽ là phiên bản mới nhất. Rất rất nhiều vấn đề xung đột thư viện xảy ra trong phát triển một bài toán AI trên nhiều máy tính khác nhau hoặc nhiều người cùng làm việc.
Vấn đề đặt ra lúc này là phải làm sao cô lập
được môi trường phát triển dành riêng cho 1 bài toán AI cụ thể. Môi trường đó phải tách biệt hoàn toàn với môi trường trên máy tính, và phải dễ dàng di chuyển giữa nhiều máy tính với nhau.
Một số công cụ đã ra đời để hỗ trợ giải quyết vấn đề này bằng cách tạo ra các môi trường ảo. Có thể kể đến như anaconda, venv, … Mỗi loại đều có những ưu nhược điểm riêng, và phụ thuộc vào thói quen sử dụng của mỗi người. Cá nhân mình cũng đã từng sử dụng qua các loại kể trên nhưng thấy chúng vẫn chưa thể giải quyết được triệt để vấn đề về xung đột môi trường …
Cho đến khi mình biết đến Docker, một công cụ rất powerfull
, rất tuyệt vời. Có thể nói docker đã giải quyết được tận gốc vấn đề làm đau đầu những nhà phát triể n AI bấy lâu nay.
Trong bài này, chúng ta sẽ cùng nhau tìm hiểu về docker và cách sử dụng nó trong viêc đóng gói một AI model để thực hiện Inference theo kiểu Online Inference
.
1. Docker là gì?
Theo định nghĩa chính thức tại trang chủ của docker thì:
Docker is an open platform for developing, shipping, and running applications.
Hiểu một cách đơn giản thì docker là một nền tảng mã nguồn mở cho việc phát triển, chạy và phân phối các ứng dụng. Nó cho phép chúng ta tách biệt ứng dụng ra khỏi kiến trúc hạ tầng chung của toàn hệ thống và dễ dang mang toàn bộ ứng dụng đó (bao gồm cả môi trường thực thi) sang một máy tính hoàn toàn mới. Điều này giúp các nhà phát triển ứng dụng giảm được thời gian đáng kể ở công đoạn đưa sản phẩm vào sử dụng trong thực tế.
Một số khái niệm cần biết khi làm việc với docker:
2. Cài đặt Docker
Để sử dụng docker thì trước tiên cần phải cài đặt docker engine
. Các cài đặt khá đơn giản, hãy làm theo hướng dẫn trên trang chủ của docker.
Sau khi cài xong docker, hãy thử chạy lệnh sau:
$ docker run tensorflow/tensorflow:2.3.0-gpu
Nếu thấy output như sau tức là ta đã cài đặt thành công:
Ở đây, tensorflow/tensorflow:2.3.0-gpu là docker image trên docker hub, được cài đặt sẵn tensorflow 2.3.0 và cuda.
Kiểm tra image vừa tải về trong danh sách:
$ docker images
Kết quả:
```python
REPOSITORY TAG IMAGE ID CREATED SIZE
tensorflow/tensorflow 2.3.0-gpu 3b8d4cbd6723 3 weeks ago 3.18GB
Nếu bạn muốn sử dụng GPU (giả sử là NVIDIA) trong docker thì bạn cần thêm 2 điều kiện:
NVIDIA Container Tookit
theo hướng dẫn ở đâyNhư trên máy tính của mình đã có đủ 2 điều kiện trên, mình kiểm tra GPU bên trong docker như sau:
$ docker run --gpus all --rm tensorflow/tensorflow:2.3.0-gpu nvidia-smi
Kết quả:
Hoặc chi tiết hơn:
$ docker run --gpus all --rm tensorflow/tensorflow:2.3.0-gpu python -c "import tensorflow as tf; print(tf.reduce_sum(tf.random.normal([1000, 1000])))"
Kết quả:
Trong đó:
Để cho phép mặc định sử dụng GPU trong docker (không cần sử dụng –gpus all), bạn có thể làm như sau:
default-runtime": "nvidia"
vào trong file /etc/docker/daemon.json
# filename: /etc/docker/daemon.json
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}
$ sudo pkill -SIGHUP dockerd
Mình đã tổng hợp lại các lệnh hay sử dụng của docker ở phần phụ lục. Các bạn có thể tham khảo thêm.
3. Xây dựng uWSGI docker image
Bên cạnh những images được xây dựng sẵn và chia sẻ trên docker hub, docker cũng hỗ trợ bạn tự build các images cho riêng bạn, phù hợp với từng nhu cầu của bạn. Tất cả được thực hiện thông qua 1 file cấu hình, gọi là Dockerfile
.
Trong phần này, ta sẽ cùng nhau xây dựng một DL docker image, bao gồm toàn bộ source code ở bài trước. Ta cũng cấu hình Flask, uWSGI trong docker image để chạy được source code đó.
3.1 Bước 1
Tạo một thư mục tên là uwsgi
, và copy toàn bộ source code của bài trước (bao gồm cả model, bỏ đi file client.py vì ta sẽ chạy client ở bên ngoài docker) vào thư mục vừa tạo.
3.2 Bước 2
Tạo file requirements.txt
chứa toàn bộ thư viện cần dùng để chạy code (cũng đặt trong thư mục uWSGI). Bạn có thể sinh ra file này tự động bằng lệnh pip freeze > requirements.txt
. Tuy nhiên, nếu làm theo cách này thì file requirements.txt
sẽ chứa rất nhiều thư viện không cần thiết, bởi vì hầu hết chúng phụ thuộc vào các thư viện khác. Nếu bạn theo dõi từ đầu, bạn chắc chắc biết rằng, ta sẽ chỉ cần những những thư viện sau là đủ: tensorflow, uwsgi, flask, opencv. Trong đó, vì mình dự định không build docker image từ đầu mà kế thừa từ 1 image đã build sẵn (cụ thể là tensorflow/tensorflow:2.3.0-gpu đã tải về ở phần trước) nên tensorflow đã được tích hợp sẵn, không cần cài lại nữa. Cuối cùng, file requirements.txt
của chúng ta chỉ như sau:
Flask==1.1.2
uWSGI==2.0.18
opencv-python==4.4.0.46
3.3 Bước 3
Tạo file cấu hình cho uWSGI. Vì mình muốn kiểm tra riêng sự hoạt động của uWSGI nên file cấu hình sẽ như sau:
[uwsgi]
http = 0.0.0.0:8080
wsgi-file = server.py
callable = app
die-on-term = true
processes = 4
threads = 2
chdir = /uwsgi
master = false
vacuum = truemodule
Trong phần sau, chúng ta sẽ kết hợp thêm Nginx. Khi đó sẽ cần thay đổi lại cấu hình của uWSGI lại một chút.
3.3 Bước 4
Tạo Dockerfile
(trong thư mục uwsgi) chứa các thông tin cấu hình cần thiết để tạo docker image. Nội dung của file này như sau:
FROM tensorflow/tensorflow:2.3.0-gpu # kế thừa từ image tensorflow/tensorflow:2.3.0-gpu
WORKDIR /uwsgi # thư mục làm viêc mặc định bên trong docker
ADD . /uwsgi # local folder để copy vào thư mục làm việc của docker, chính là thư mục chúng ta tạo ở bước 1
RUN pip install -r requirements.txt # cài đặt các thư viện cần thiết trong file requirements.txt
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y # sửa lỗi opencv, thử bỏ đi để xem điều gì xảy ra?
CMD ["uwsgi", "app.ini"] # chạy lệnh "uwsgi app.ini" khi khởi chạy docker image
Có rất rất nhiều tùy chọn khi viết Dockerfile, tham khảo ở đây nếu bạn cần thêm thông tin.
Thự mục uwsgi
lúc này sẽ như sau:
├── animal_model_classification.h5
├── app.ini
├── cat.1.jpg
├── Dockerfile
├── dog.1.jpg
├── requirements.txt
└── server.py
Trong đó, 2 files ảnh là để chúng ta thực hiện việc test về sau.
4 Build docker image
OK, mọi thứ cần thiết đã chuẩn bị xong, ta sẽ chạy lệnh sau để buidl docker image:
$ docker build -t image-classification-production:1.0 .
Docker image được sinh ra sẽ có tên là image-classification-production
, kèm theo tag 1.0
để phân biệt nó với các phiên bản khác trong tương lai.
Nếu build, thành công, output sẽ như sau:
---> a2a60f32471b
Step 7/7 : CMD ["uwsgi", "app.ini"]
---> Running in 3776d496ca68
Removing intermediate container 3776d496ca68
---> 53150d1373a3
Successfully built 53150d1373a3
Successfully tagged image-classification-production:1.0
Kiểm tra docker image trong danh sách:
docker images
Kết quả:
REPOSITORY TAG IMAGE ID CREATED SIZE
image-classification-production 1.0 53150d1373a3 About a minute ago 5.37GB
tensorflow/tensorflow 2.3.0-gpu 3b8d4cbd6723 3 weeks ago 3.18GB
5 Chạy docker image
Để chạy docker image vừa tạo, ta sử dụng lệnh sau:
$ docker run --rm --publish 80:8080 --name dlp image-classification-production:1.0
Có 2 cái mà ta phải chú ý ở đây:
--public 80:8080
sẽ “expose” port 8080 của container tới port 80 của host. Nói cách khác, tất cả các requests đến địa chỉ localhost:80 sẽ được chuyển tiếp đến địa chỉ 0.0.0.0:8080 bên trong container. 8080 được gọi là listening port
của uWSGI.--name dlp
sẽ đặt tên cho container là dlp
. Ta nên đặt tên cho container để dễ làm việc với nó hơn. Ngược lại, docker sẽ tạo cho nó một ID ngẫu nhiên.Kết quả:
2021-01-06 02:52:45.347188: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:982] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-01-06 02:52:45.347580: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:982] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-01-06 02:52:45.347925: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1402] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 4676 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1660 Ti with Max-Q Design, pci bus id: 0000:01:00.0, compute capability: 7.5)
2021-01-06 02:52:47.271932: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
2021-01-06 02:52:47.621364: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
2021-01-06 02:52:47.918398: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
2021-01-06 02:52:48.759371: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
2021-01-06 02:52:49.637126: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
WSGI app 0 (mountpoint='') ready in 8 seconds on interpreter 0x55cf948bc720 pid: 1 (default app)
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 1, cores: 1)
Ta quan sá thấy container đã chạy thành công và uWSGI cũng đã được khởi động.
Lưu ý: Trong trường hợp port 80 đã được sử dụng bới ứng dụng khác (nginx trong bài trước chẳng hạn), bạn phải close ứng dụng đó hoặc sử dụng một port khác.
Để kiểm tra xem uWSGI có làm viêc đúng hay không, ta có thể sử dụng lại client đã chuẩn bị từ bài trước (nhớ đổi port của ENDPOINT_URL từ 8080 thành 80).
$ python client.py
Kết quả:
b'cat'
Như vậy là uWSGI đã được cài đặt thành công vào docker.
Lưu ý: Trong quá trình viết bài này, mình gặp lỗi liên quan đến cuda khi chạy test uWSGI. Mình xóa hết các docker images đi và chạy lại từ đầu thì không bị lỗi nữa.
6. Xây dựng Nginx docker image
Tương tự như việc xây dựng uWSGI docker image, chúng ta sẽ đi build một Nginx docker image, đặt trước uWSGI server thực hiện vai trò như một reverse proxy
.
6.1 Bước 1
Tạo thư mục nginx
, cùng cấp với thư mục uwsgi
.
6.2 Bước 2
Tạo file cấu hình Nginx, tên là nginx.conf
, đặt trong thư mục nginx
, với nội dung như sau:
server {
listen 80;
location / {
include uwsgi_params;
uwsgi_pass uwsgi:660 ;
}
Với cấu hình này thì Nginx sẽ lắng nghe trên port 80, chuyển tiếp các requests đến port 660 của uWSGI server thông qua socket (sử dụng giao thức uwsgi).
6.3 Bước 3
Cập nhật lại cấu hình của uWSGI (file app.ini) để làm việc được với Nginx, như sau:
module = server
socket= :660
callable = app
die-on-term = true
processes = 1
master = false
vacuum = true
6.4 Bước 4
Tạo file Dockerfile cho Nginix docker trong thư mục nginx
. Nginix docker image được kế thừa từ nginx image trên docker hub, ta chỉ việc thay thế cấu hình mặc định của nó bằng cấu hình mà ta vừa tạo ở bước 3.
FROM nginx
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/
Đến đây, nếu chạy lệnh docker build ...
thì ta sẽ có được nginx docker image. Nhưng nếu chỉ chạy một mình image này thì không có tác dụng gì cả. Ta cần phải kết hợp cả 2 docker images uwsgi và nginx. Đó chính là công viêc của docker-compose
.
7. Chạy đồng thời nhiều docker containers với Docker Compose
Liệu bạn có thắc mắc rằng tại sao ta không build cả Nginx và uWSGI vào chung 1 docker image? Chẳng phải như thế sẽ tiện hơn hay sao?
Câu trả lời là không nên làm vậy. Theo kiến trúc làm việc kết hợp giữa Nginx và uWSGI thì một Nginx instance có thể kết hợp với nhiề u uWSGI instances. Nếu ta kết hợp chung lại, sẽ không tận dụng được khả năng này. Thêm nữa, dung lượng của docker image sẽ rất lớn nếu ta kết hợp lại.
Mở rộng ra, nếu một hệ thống của chúng ta bao gồm cả database, backend, front-end, messaging systems, task queue, … ta không thể chạy tất tần tật mọi thứ trong một docker container được.
Từ góc độ của nhà phát triển phần mềm, docker-compose chỉ là một file cấu hình, định nghĩa tất cả containers và cách thức mà các containers đó tương tác với nhau.
7.1 Cài đặt docker-compose
Để cài đặt docker-compose. Bạn hãy làm theo hướng dẫn sau trên trang chủ của docker.
7.2 Định nghĩa cấu hình của docker-compose
Tạo file docker-compose.yml
(bên ngoài 2 thư mục uwsgi và nginx), với nội dung như sau:
version : "3.7"
services:
uwsgi:
build: ./uwsgi
container_name: uwsgi_img_classification
restart: always
expose:
- 660
nginx:
build: ./nginx
container_name: nginx
restart: always
ports:
- "80:80"
Phần chính của cấu hình này là khai báo 2 containers, gọi là 2 services. Hai tham số quan trọng của mỗi services là:
Như vậy, có thể tóm tắt lại flow như sau:
7.3 Build docker-compose
Chạy lệnh sau để build docker-compose với cả 2 containers.
$ docker-compose build
Nếu build thành công, output sẽ như sau:
Step 7/7 : CMD ["uwsgi", "app.ini"]
---> Running in 9608e1187e82
Removing intermediate container 9608e1187e82
---> 357fe8e41768
Successfully built 357fe8e41768
Successfully tagged docker_uwsgi:latest
Building nginx
Step 1/3 : FROM nginx
latest: Pulling from library/nginx
6ec7b7d162b2: Pull complete
cb420a90068e: Pull complete
2766c0bf2b07: Pull complete
e05167b6a99d: Pull complete
70ac9d795e79: Pull complete
Digest: sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9
Status: Downloaded newer image for nginx:latest
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Running in 11140e051282
Removing intermediate container 11140e051282
---> 1fcc92cfdfc4
Step 3/3 : COPY nginx.conf /etc/nginx/conf.d/
---> 21bda0089cca
Successfully built 21bda0089cca
Successfully tagged docker_nginx:latest
Kiểm tra thử danh sách images bằng lệnh docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
docker_nginx latest 21bda0089cca 4 minutes ago 133MB
docker_uwsgi latest 357fe8e41768 5 minutes ago 5.37GB
image-classification-production 1.0 bd9928abee21 2 hours ago 5.37GB
nginx latest ae2feff98a0c 3 weeks ago 133MB
tensorflow/tensorflow 2.3.0-gpu 3b8d4cbd6723 3 weeks ago 3.18GB
Ta thấy hai containers docker_nginx
và docker_uwsgi
đã xuất hiện.
7.4 Kiểm tra hoạt động của hệ thống
Ta sẽ khởi động các containers lên để kiểm tra thử xem hê thống có làm việc chính xác không.
$ docker-compose up
Khởi động thành công:
Starting nginx ... done
Starting uwsgi_img_classification ... done
Attaching to nginx, uwsgi_img_classification
nginx | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
nginx | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
uwsgi_img_classification | [uWSGI] getting INI configuration from app.ini
.....
uwsgi_img_classification | 2021-01-06 09:18:12.292414: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1402] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 4662 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1660 Ti with Max-Q Design, pci bus id: 0000:01:00.0, compute capability: 7.5)
uwsgi_img_classification | 2021-01-06 09:18:14.386546: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
uwsgi_img_classification | 2021-01-06 09:18:14.737805: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
uwsgi_img_classification | 2021-01-06 09:18:15.045424: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
uwsgi_img_classification | 2021-01-06 09:18:16.238951: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
uwsgi_img_classification | 2021-01-06 09:18:17.246651: W tensorflow/core/framework/cpu_allocator_impl.cc:81] Allocation of 536870912 exceeds 10% of free system memory.
uwsgi_img_classification | WSGI app 0 (mountpoint='') ready in 7 seconds on interpreter 0x56494b98c680 pid: 1 (default app)
uwsgi_img_classification | uWSGI running as root, you can use --uid/--gid/--chroot options
uwsgi_img_classification | *** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
uwsgi_img_classification | *** uWSGI is running in multiple interpreter mode ***
uwsgi_img_classification | spawned uWSGI worker 1 (and the only) (pid: 1, cores: 1)
Chạy client để nhận diện: python client.py
.
Kết quả:
b'cat'
8. Kết luận
Phù, thật tuyệt vời, mọi thứ đã chạy đúng như mong muốn.
Bài hôm nay khá là dài và khó. Mình đã phải thực hiện cài cắm rất nhiều lần để có thể hoàn thành bài viết này. Hi vọng sẽ có ích cho các bạn trong việc tìm kiếm giải pháp triể n khải AI model vào trong các sản phẩm để đưa đến tay người dùng!
Toàn bộ source code sử dụng trong bài này, các bạn có thể tham khảo trên github cá nhân của mình tại đây. Giống như bài trước, vì model animal_model_classification.h5
có dung lượng khá lớn (> 1.5GB) nên mình không upload lên github được. Các bạn hãy sử dụng model của chính mình để thực hành nhé!
Trong các bài viết tiếp theo, mình sẽ sử dụng docker để train một model khác và thực hiện batch inference
bằng model đó. Mời các bạn đón đọc!
9. Phụ lục một số lệnh cơ bản của Docker
1. List docker image
$ docker images
2. List container
$ docker ps <-a>
3. Run a docker
$ docker run -it [image_name] bash
4. Access to running container
$ docker exec -it [container_id or container_name] bash
5. Commit change of container to docker image
$ docker commit [container_name or container_id] [new_image_name]
6. Stop running container
$ docker stop [container_id or container_name]
$ docker stop $(docker ps -aq) # Stop all container
7. Start stoped container
$ docker start [container_id or container_name]
8. Remove container
$ docker rm [container_id or container_name]
$ docker rm $(docker ps -aq) # Remove all
9. Export container
$ docker export [container_id or container_name] | gzip > file_export.tar.gz
10. Import docker => images
$ zcat file_export.tar.gz | docker [new_name_image]
$ docker images # check
11. Remove docker image
$ docker rmi [image_name]
Loi: docker: Error response from daemon: Unknown runtime specified nvidia.
Solution:
1.
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
2.
$ sudo mkdir -p /etc/systemd/system/docker.service.d
$ sudo tee /etc/systemd/system/docker.service.d/override.conf <<EOF
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --host=fd:// --add-runtime=nvidia=/usr/bin/nvidia-container-runtime
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
** Move docker image to other computer
1. Save images
$ docker save <REPOSITORY> > <images_name>.tar
2. Load images
$ docker load < <images_name>.tar
3. Run images
$ docker run -it --runtime=nvidia --rm --net=host --privileged <Image ID>
10. Tham khảo