本文以开源数据集fashion-mnist任务为例,介绍开发者如何利用云原生AI套件,在ACK集群运行深度学习任务,优化分布式训练性能,调试模型效果,并最终把模型部署到ACK集群中。
背景信息
云原生AI套件包括一系列可单独部署的组件(K8s Helm Chart),辅助AI工程加速。
- 管理员:负责管理用户及其权限,分配集群资源,配置外部存储,管理数据集,并通过集群大盘观测集群使用情况。
- 开发者:主要使用集群资源提交任务,开发者需要被管理员创建并分配权限,之后便可以选择使用Arena命令行、Jupyter Notebook等工具进行日常开发。
前提条件
管理员已完成以下操作:
- 已创建Kubernetes集群。具体操作,请参见创建Kubernetes托管版集群。
- 集群每个节点的磁盘空间大于等于300 GB。
- 若希望取得最佳的数据加速对比效果,可以选择4机8卡v100机型进行实验。
- 若希望取得最佳的拓扑感知效果,可以选择两台v100机型进行实验。
- 已安装云原生AI套件并部署所有组件。具体操作,请参见安装云原生AI套件。
- 可访问AI运维控制台。关于如何配置AI运维控制台,请参见访问AI运维控制台。
- 可访问AI开发控制台。关于如何配置AI开发控制台,请参见访问AI开发控制台。
- 下载Fashion MNIST数据集,并上传至OSS。关于如何将模型上传到OSS上,请参见控制台上传文件。
- 已获取测试代码的Git仓库地址,及用户名和密码。
- 已通过Kubectl工具连接Kubernetes集群。具体操作,请参见获取集群KubeConfig并通过kubectl工具连接集群。
- 已安装命令行提交工具Arena。具体操作,请参见安装Arena。
实验环境
- 首先需要管理员执行,步骤一:为开发者创建账号并分配资源、步骤二:创建数据集。其余步骤可由开发者完成。
- 开发者需要在Jupyter Notebook中新建Terminal或使用集群中的跳板机提交Arena命令行,推荐使用Jupyter Notebook。
主机名 | IP | 角色 | GPU卡数 | CPU核数 | Memory |
cn-beijing.192.168.0.13 | 192.168.0.13 | 跳板机 | 1 | 8 | 30580004 KiB |
cn-beijing.192.168.0.16 | 192.168.0.16 | Worker | 1 | 8 | 30580004 KiB |
cn-beijing.192.168.0.17 | 192.168.0.17 | Worker | 1 | 8 | 30580004 KiB |
cn-beijing.192.168.0.240 | 192.168.0.240 | Worker | 1 | 8 | 30580004 KiB |
cn-beijing.192.168.0.239 | 192.168.0.239 | Worker | 1 | 8 | 30580004 KiB |
实验目标
- 数据集管理
- 使用Jupyter Notebook搭建开发环境
- 提交单机训练任务
- 提交分布式训练任务
- 使用Fluid加速训练任务
- 使用ACK AI任务调度器加速训练任务
- 模型管理
- 模型评测
- 部署推理服务
步骤一:为开发者创建账号并分配资源
- 用户账号和密码。关于如何新增用户,请参见管理用户。
- 资源配额。关于如何分配资源配额,请参见管理弹性配额组。
- 若通过AI开发控制台提交任务,请获取AI开发控制台的访问地址。关于如何访问AI开发控制台,请参见访问AI开发控制台。
- 若通过Arena命令行提交任务,请获取访问集群的kube.config。关于如何获取访问集群的kube.config,请参见步骤二:选择集群凭证类型。
步骤二:创建数据集
数据集需要由管理员角色来管理。本示例使用fashion-mnist数据集。
步骤一:创建fashion-mnist数据集
- 根据以下YAML示例创建fashion-mnist.yaml文件。本示例创建OSS类型的PV及PVC。
apiVersion: v1 kind: PersistentVolume metadata: name: fashion-demo-pv spec: accessModes: - ReadWriteMany capacity: storage: 10Gi csi: driver: ossplugin.csi.alibabacloud.com volumeAttributes: bucket: fashion-mnist otherOpts: "" url: oss-cn-beijing.aliyuncs.com akId: "AKID" akSecret: "AKSECRET" volumeHandle: fashion-demo-pv persistentVolumeReclaimPolicy: Retain storageClassName: oss volumeMode: Filesystem --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: fashion-demo-pvc namespace: demo-ns spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi selector: matchLabels: alicloud-pvname: fashion-demo-pv storageClassName: oss volumeMode: Filesystem volumeName: fashion-demo-pv
- 执行以下命令创建fashion-mnist数据集。
kubectl create -f fashion-mnist.yaml
- 查看PV及PVC的状态。
- 执行以下命令,查看PV的状态。
kubectl get pv fashion-mnist-jackwg
预期输出:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE fashion-mnist-jackwg 10Gi RWX Retain Bound ns1/fashion-mnist-jackwg-pvc oss 8h
- 执行以下命令,查看PVC的状态。
kubectl get pvc fashion-mnist-jackwg-pvc -n ns1
预期输出:NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE fashion-mnist-jackwg-pvc Bound fashion-mnist-jackwg 10Gi RWX oss 8h
- 执行以下命令,查看PV的状态。
步骤二:创建加速数据集
管理员可通过AI运维控制台创建加速数据集。
- 使用管理员账号访问AI运维控制台。
- 在AI运维控制台左侧导航栏中,选择 。
- 在数据集列表页面,单击目标数据集右侧操作列的一键加速。加速后的数据集如下图所示:
步骤三:模型开发
- 使用自定义镜像创建Jupyter Notebook(可选)。
- 通过Jupyter Notebook开发测试。
- 在Jupyter Notebook中提交代码至Git仓库。
- 使用Arena SDK提交训练任务。
(可选)步骤一:使用自定义镜像创建Jupyter Notebook
AI开发控制台的Jupyter Notebook,默认提供了Tensorflow及Pytorch不同版本的镜像,若均不满足需求可考虑自定义镜像。
- 使用以下Dockerfile模板样例,创建名为Dockerfile的文件。关于自定义镜像的规范,请参见创建并使用Notebook。
cat<<EOF >dockerfile FROM tensorflow/tensorflow:1.15.5-gpu USER root RUN pip install jupyter && \ pip install ipywidgets && \ jupyter nbextension enable --py widgetsnbextension && \ pip install jupyterlab && jupyter serverextension enable --py jupyterlab EXPOSE 8888 #USER jovyan CMD ["sh", "-c", "jupyter-lab --notebook-dir=/home/jovyan --ip=0.0.0.0 --no-browser --allow-root --port=8888 --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.allow_origin='*' --NotebookApp.base_url=${NB_PREFIX} --ServerApp.authenticate_prometheus=False"] EOF
- 执行以下命令,使用Dockerfile构建镜像。
docker build -f dockerfile .
预期输出:Sending build context to Docker daemon 9.216kB Step 1/5 : FROM tensorflow/tensorflow:1.15.5-gpu ---> 73be11373498 Step 2/5 : USER root ---> Using cache ---> 7ee21dc7e42e Step 3/5 : RUN pip install jupyter && pip install ipywidgets && jupyter nbextension enable --py widgetsnbextension && pip install jupyterlab && jupyter serverextension enable --py jupyterlab ---> Using cache ---> 23bc51c5e16d Step 4/5 : EXPOSE 8888 ---> Using cache ---> 76a55822ddae Step 5/5 : CMD ["sh", "-c", "jupyter-lab --notebook-dir=/home/jovyan --ip=0.0.0.0 --no-browser --allow-root --port=8888 --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.allow_origin='*' --NotebookApp.base_url=${NB_PREFIX} --ServerApp.authenticate_prometheus=False"] ---> Using cache ---> 3692f04626d5 Successfully built 3692f04626d5
- 执行以下命令,推送镜像到您的Docker镜像仓库。
docker tag ${IMAGE_ID} registry-vpc.cn-beijing.aliyuncs.com/${DOCKER_REPO}/jupyter:fashion-mnist-20210802a docker push registry-vpc.cn-beijing.aliyuncs.com/${DOCKER_REPO}/jupyter:fashion-mnist-20210802a
- 创建拉取镜像所需Docker仓库的Secret。更多信息,请参见在集群中创建保存授权令牌的Secret。
kubectl create secret docker-registry regcred \ --docker-server=<您的镜像仓库服务器> \ --docker-username=<您的用户名> \ --docker-password=<您的密码> \ --docker-email=<您的邮箱地址>
- 在AI开发控制台创建Jupyter Notebook。关于如何创建Jupyter Notebook,请参见创建并使用Notebook。创建Jupyter Notebook的相关参数配置请参见下图:
步骤二:通过Jupyter Notebook开发测试
- 访问AI开发控制台。
- 在AI开发控制台的左侧导航栏中,单击Notebook。
- 在Notebook页面,单击状态为Running的目标Jupyter Notebook。
- 新建命令行Launcher,确认数据是否挂载成功。
pwd /root/data ls -alh
预期输出:total 30M drwx------ 1 root root 0 Jan 1 1970 . drwx------ 1 root root 4.0K Aug 2 04:15 .. drwxr-xr-x 1 root root 0 Aug 1 14:16 saved_model -rw-r----- 1 root root 4.3M Aug 1 01:53 t10k-images-idx3-ubyte.gz -rw-r----- 1 root root 5.1K Aug 1 01:53 t10k-labels-idx1-ubyte.gz -rw-r----- 1 root root 26M Aug 1 01:54 train-images-idx3-ubyte.gz -rw-r----- 1 root root 29K Aug 1 01:53 train-labels-idx1-ubyte.gz
- 创建供开发fashion-mnist模型使用的Jupyter Notebook,初始化内容如下所示:
#!/usr/bin/python # -*- coding: UTF-8 -*- import os import gzip import numpy as np import tensorflow as tf from tensorflow import keras print('TensorFlow version: {}'.format(tf.__version__)) dataset_path = "/root/data/" model_path = "./model/" model_version = "v1" def load_data(): files = [ 'train-labels-idx1-ubyte.gz', 'train-images-idx3-ubyte.gz', 't10k-labels-idx1-ubyte.gz', 't10k-images-idx3-ubyte.gz' ] paths = [] for fname in files: paths.append(os.path.join(dataset_path, fname)) with gzip.open(paths[0], 'rb') as labelpath: y_train = np.frombuffer(labelpath.read(), np.uint8, offset=8) with gzip.open(paths[1], 'rb') as imgpath: x_train = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(y_train), 28, 28) with gzip.open(paths[2], 'rb') as labelpath: y_test = np.frombuffer(labelpath.read(), np.uint8, offset=8) with gzip.open(paths[3], 'rb') as imgpath: x_test = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(y_test), 28, 28) return (x_train, y_train),(x_test, y_test) def train(): (train_images, train_labels), (test_images, test_labels) = load_data() # scale the values to 0.0 to 1.0 train_images = train_images / 255.0 test_images = test_images / 255.0 # reshape for feeding into the model train_images = train_images.reshape(train_images.shape[0], 28, 28, 1) test_images = test_images.reshape(test_images.shape[0], 28, 28, 1) class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] print('\ntrain_images.shape: {}, of {}'.format(train_images.shape, train_images.dtype)) print('test_images.shape: {}, of {}'.format(test_images.shape, test_images.dtype)) model = keras.Sequential([ keras.layers.Conv2D(input_shape=(28,28,1), filters=8, kernel_size=3, strides=2, activation='relu', name='Conv1'), keras.layers.Flatten(), keras.layers.Dense(10, activation=tf.nn.softmax, name='Softmax') ]) model.summary() testing = False epochs = 5 model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) logdir = "/training_logs" tensorboard_callback = keras.callbacks.TensorBoard(log_dir=logdir) model.fit(train_images, train_labels, epochs=epochs, callbacks=[tensorboard_callback], ) test_loss, test_acc = model.evaluate(test_images, test_labels) print('\nTest accuracy: {}'.format(test_acc)) export_path = os.path.join(model_path, model_version) print('export_path = {}\n'.format(export_path)) tf.keras.models.save_model( model, export_path, overwrite=True, include_optimizer=True, save_format=None, signatures=None, options=None ) print('\nSaved model success') if __name__ == '__main__': train()
重要 代码中的dataset_path及model_path需要指定为Notebook挂载的数据源路径,Notebook就可以访问挂载到本地文件中的数据集。 - 在目标Notebook中单击图标。预期输出:
TensorFlow version: 1.15.5 train_images.shape: (60000, 28, 28, 1), of float64 test_images.shape: (10000, 28, 28, 1), of float64 Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= Conv1 (Conv2D) (None, 13, 13, 8) 80 _________________________________________________________________ flatten_2 (Flatten) (None, 1352) 0 _________________________________________________________________ Softmax (Dense) (None, 10) 13530 ================================================================= Total params: 13,610 Trainable params: 13,610 Non-trainable params: 0 _________________________________________________________________ Train on 60000 samples Epoch 1/5 60000/60000 [==============================] - 3s 57us/sample - loss: 0.5452 - acc: 0.8102 Epoch 2/5 60000/60000 [==============================] - 3s 52us/sample - loss: 0.4103 - acc: 0.8555 Epoch 3/5 60000/60000 [==============================] - 3s 55us/sample - loss: 0.3750 - acc: 0.8681 Epoch 4/5 60000/60000 [==============================] - 3s 55us/sample - loss: 0.3524 - acc: 0.8757 Epoch 5/5 60000/60000 [==============================] - 3s 53us/sample - loss: 0.3368 - acc: 0.8798 10000/10000 [==============================] - 0s 37us/sample - loss: 0.3770 - acc: 0.8673 Test accuracy: 0.8672999739646912 export_path = ./model/v1 Saved model success
步骤三:在Jupyter Notebook中提交代码至Git仓库
Jupyter Notebook创建完成后,可以直接在Notebook中提交代码到您的Git库。
- 执行以下命令,安装Git。
apt-get update apt-get install git
- 执行以下命令,初始化配置Git并保存用户名密码至Notebook。
git config --global credential.helper store git pull ${YOUR_GIT_REPO}
- 执行以下命令,推送代码到Git库。
git push origin fashion-test
预期输出:Total 0 (delta 0), reused 0 (delta 0) To codeup.aliyun.com:60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git * [new branch] fashion-test -> fashion-test
步骤四:使用Arena SDK提交训练任务
- 安装Arena SDK的依赖。
!pip install coloredlogs
- 根据以下示例新建Python文件,初始化代码如下所示:
import os import sys import time from arenasdk.client.client import ArenaClient from arenasdk.enums.types import * from arenasdk.exceptions.arena_exception import * from arenasdk.training.tensorflow_job_builder import * from arenasdk.logger.logger import LoggerBuilder def main(): print("start to test arena-python-sdk") client = ArenaClient("","demo-ns","info","arena-system") # demo-ns是提交到的namespace。 print("create ArenaClient succeed.") print("start to create tfjob") job_name = "arena-sdk-distributed-test" job_type = TrainingJobType.TFTrainingJob try: # build the training job job = TensorflowJobBuilder().with_name(job_name)\ .witch_workers(1)\ .with_gpus(1)\ .witch_worker_image("tensorflow/tensorflow:1.5.0-devel-gpu")\ .witch_ps_image("tensorflow/tensorflow:1.5.0-devel")\ .witch_ps_count(1)\ .with_datas({"fashion-demo-pvc":"/data"})\ .enable_tensorboard()\ .with_sync_mode("git")\ .with_sync_source("https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git")\ #Git仓库地址。 .with_envs({\ "GIT_SYNC_USERNAME":"USERNAME", \ #Git仓库用户名。 "GIT_SYNC_PASSWORD":"PASSWORD",\ #Git仓库密码。 "TEST_TMPDIR":"/",\ })\ .with_command("python code/tensorflow-fashion-mnist-sample/tf-distributed-mnist.py").build() # if training job is not existed,create it if client.training().get(job_name, job_type): print("the job {} has been created, to delete it".format(job_name)) client.training().delete(job_name, job_type) time.sleep(3) output = client.training().submit(job) print(output) count = 0 # waiting training job to be running while True: if count > 160: raise Exception("timeout for waiting job to be running") jobInfo = client.training().get(job_name,job_type) if jobInfo.get_status() == TrainingJobStatus.TrainingJobPending: print("job status is PENDING,waiting...") count = count + 1 time.sleep(5) continue print("current status is {} of job {}".format(jobInfo.get_status().value,job_name)) break # get the training job logs logger = LoggerBuilder().with_accepter(sys.stdout).with_follow().with_since("5m") #jobInfo.get_instances()[0].get_logs(logger) # display the training job information print(str(jobInfo)) # delete the training job #client.training().delete(job_name, job_type) except ArenaException as e: print(e) main()
namespace
:本示例是将训练任务提交到demo-ns命名空间下。with_sync_source
:Git仓库地址。with_envs
:Git仓库用户名和密码。
- 在目标Notebook中单击图标。预期输出:
2021-11-02/08:57:28 DEBUG util.py[line:19] - execute command: [arena get --namespace=demo-ns --arena-namespace=arena-system --loglevel=info arena-sdk-distributed-test --type=tfjob -o json] 2021-11-02/08:57:28 DEBUG util.py[line:19] - execute command: [arena submit --namespace=demo-ns --arena-namespace=arena-system --loglevel=info tfjob --name=arena-sdk-distributed-test --workers=1 --gpus=1 --worker-image=tensorflow/tensorflow:1.5.0-devel-gpu --ps-image=tensorflow/tensorflow:1.5.0-devel --ps=1 --data=fashion-demo-pvc:/data --tensorboard --sync-mode=git --sync-source=https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git --env=GIT_SYNC_USERNAME=kubeai --env=GIT_SYNC_PASSWORD=kubeai@ACK123 --env=TEST_TMPDIR=/ python code/tensorflow-fashion-mnist-sample/tf-distributed-mnist.py] start to test arena-python-sdk create ArenaClient succeed. start to create tfjob 2021-11-02/08:57:29 DEBUG util.py[line:19] - execute command: [arena get --namespace=demo-ns --arena-namespace=arena-system --loglevel=info arena-sdk-distributed-test --type=tfjob -o json] service/arena-sdk-distributed-test-tensorboard created deployment.apps/arena-sdk-distributed-test-tensorboard created tfjob.kubeflow.org/arena-sdk-distributed-test created job status is PENDING,waiting... 2021-11-02/09:00:34 DEBUG util.py[line:19] - execute command: [arena get --namespace=demo-ns --arena-namespace=arena-system --loglevel=info arena-sdk-distributed-test --type=tfjob -o json] current status is RUNNING of job arena-sdk-distributed-test { "allocated_gpus": 1, "chief_name": "arena-sdk-distributed-test-worker-0", "duration": "185s", "instances": [ { "age": "13s", "gpu_metrics": [], "is_chief": false, "name": "arena-sdk-distributed-test-ps-0", "node_ip": "192.168.5.8", "node_name": "cn-beijing.192.168.5.8", "owner": "arena-sdk-distributed-test", "owner_type": "tfjob", "request_gpus": 0, "status": "Running" }, { "age": "13s", "gpu_metrics": [], "is_chief": true, "name": "arena-sdk-distributed-test-worker-0", "node_ip": "192.168.5.8", "node_name": "cn-beijing.192.168.5.8", "owner": "arena-sdk-distributed-test", "owner_type": "tfjob", "request_gpus": 1, "status": "Running" } ], "name": "arena-sdk-distributed-test", "namespace": "demo-ns", "priority": "N/A", "request_gpus": 1, "tensorboard": "http://192.168.5.6:31068", "type": "tfjob" }
步骤四:模型训练
根据以下示例提交Tensorflow单机训练任务、Tensorflow分布式训练任务、Fluid加速训练任务及ACK AI任务调度器加速分布式训练任务。
示例一:提交Tensorflow单机训练任务
Notebook开发完成并保存代码后,可通过Arena命令行或AI开发控制台两种方式提交训练任务。
方式一:通过Arena命令行提交训练任务
arena \
submit \
tfjob \
-n ns1 \
--name=fashion-mnist-arena \
--data=fashion-mnist-jackwg-pvc:/root/data/ \
--env=DATASET_PATH=/root/data/ \
--env=MODEL_PATH=/root/saved_model \
--env=MODEL_VERSION=1 \
--env=GIT_SYNC_USERNAME=<GIT_USERNAME> \
--env=GIT_SYNC_PASSWORD=<GIT_PASSWORD> \
--sync-mode=git \
--sync-source=https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git \
--image="tensorflow/tensorflow:2.2.2-gpu" \
"python /root/code/tensorflow-fashion-mnist-sample/train.py --log_dir=/training_logs"
方式二:通过AI开发控制台提交训练任务
- 配置数据源。具体操作,请参见配置训练数据。
部分配置参数说明如下所示:
参数名 示例值 是否必填 名称 fashion-demo 是 命名空间 demo-ns 是 存储卷声明 fashion-demo-pvc 是 本地存储目录 /root/data 否 - 配置代码源。具体操作,请参见配置训练代码。
参数名 示例值 是否必填 名称 fashion-git 是 Git地址 https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git 是 默认分支 master 否 本地存储目录 /root/ 否 私有Git用户名 您的私有Git仓库的用户名 否 私有Git密码/凭证 您的私有Git仓库的密码 否 - 提交单机训练任务。具体操作,请参见提交Tensorflow训练任务。配置训练参数后,单击提交,然后即可在任务列表查看到训练任务。提交Job的参数如下所示:
参数名 说明 任务名称 本示例的任务名称为fashion-tf-ui。 任务类型 本示例选择Tensorflow单机。 命名空间 本示例为demo-ns。与数据集所在命名空间必须相同。 数据源配置 本示例为fashion-demo,选择步骤1中的配置。 代码配置 本示例为fashion-git,选择步骤2中的配置。 代码分支 本示例为master。 执行命令 本示例为 "export DATASET_PATH=/root/data/ &&export MODEL_PATH=/root/saved_model &&export MODEL_VERSION=1 &&python /root/code/tensorflow-fashion-mnist-sample/train.py"
。私有Git仓库 若需要使用私有的代码库,需要配置私有Git仓库的用户名和密码。 实例数量 默认为1。 镜像 本示例为 tensorflow/tensorflow:2.2.2-gpu
。镜像拉取凭证 若需要使用私有的镜像仓库,需要提前创建Secret。 CPU(核数) 默认为4。 内存(GB) 默认为8。 关于更多的Arena命令行参数,请参见Arena提交TFJob。
- 任务提交完成后,查看任务日志。
- 在AI开发控制台的左侧导航栏中,单击任务列表。
- 在任务列表页面,单击目标任务名称。
- 在目标任务详情页面,单击实例页签,然后单击目标实例右侧操作列的日志。本示例的日志信息如下所示:
train_images.shape: (60000, 28, 28, 1), of float64 test_images.shape: (10000, 28, 28, 1), of float64 Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= Conv1 (Conv2D) (None, 13, 13, 8) 80 _________________________________________________________________ flatten (Flatten) (None, 1352) 0 _________________________________________________________________ Softmax (Dense) (None, 10) 13530 ================================================================= Total params: 13,610 Trainable params: 13,610 Non-trainable params: 0 _________________________________________________________________ Epoch 1/5 2021-08-01 14:21:17.532237: E tensorflow/core/profiler/internal/gpu/cupti_tracer.cc:1430] function cupti_interface_->EnableCallback( 0 , subscriber_, CUPTI_CB_DOMAIN_DRIVER_API, cbid)failed with error CUPTI_ERROR_INVALID_PARAMETER 2021-08-01 14:21:17.532390: I tensorflow/core/profiler/internal/gpu/device_tracer.cc:216] GpuTracer has collected 0 callback api events and 0 activity events. 2021-08-01 14:21:17.533535: I tensorflow/core/profiler/rpc/client/save_profile.cc:168] Creating directory: /training_logs/train/plugins/profile/2021_08_01_14_21_17 2021-08-01 14:21:17.533928: I tensorflow/core/profiler/rpc/client/save_profile.cc:174] Dumped gzipped tool data for trace.json.gz to /training_logs/train/plugins/profile/2021_08_01_14_21_17/fashion-mnist-arena-chief-0.trace.json.gz 2021-08-01 14:21:17.534251: I tensorflow/core/profiler/utils/event_span.cc:288] Generation of step-events took 0 ms 2021-08-01 14:21:17.534961: I tensorflow/python/profiler/internal/profiler_wrapper.cc:87] Creating directory: /training_logs/train/plugins/profile/2021_08_01_14_21_17Dumped tool data for overview_page.pb to /training_logs/train/plugins/profile/2021_08_01_14_21_17/fashion-mnist-arena-chief-0.overview_page.pb Dumped tool data for input_pipeline.pb to /training_logs/train/plugins/profile/2021_08_01_14_21_17/fashion-mnist-arena-chief-0.input_pipeline.pb Dumped tool data for tensorflow_stats.pb to /training_logs/train/plugins/profile/2021_08_01_14_21_17/fashion-mnist-arena-chief-0.tensorflow_stats.pb Dumped tool data for kernel_stats.pb to /training_logs/train/plugins/profile/2021_08_01_14_21_17/fashion-mnist-arena-chief-0.kernel_stats.pb 1875/1875 [==============================] - 3s 2ms/step - loss: 0.5399 - accuracy: 0.8116 Epoch 2/5 1875/1875 [==============================] - 3s 2ms/step - loss: 0.4076 - accuracy: 0.8573 Epoch 3/5 1875/1875 [==============================] - 3s 2ms/step - loss: 0.3727 - accuracy: 0.8694 Epoch 4/5 1875/1875 [==============================] - 3s 2ms/step - loss: 0.3512 - accuracy: 0.8769 Epoch 5/5 1875/1875 [==============================] - 3s 2ms/step - loss: 0.3351 - accuracy: 0.8816 313/313 [==============================] - 0s 1ms/step - loss: 0.3595 - accuracy: 0.8733 2021-08-01 14:21:34.820089: W tensorflow/python/util/util.cc:329] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them. WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/resource_variable_ops.py:1817: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version. Instructions for updating: If using Keras pass *_constraint arguments to layers. Test accuracy: 0.8733000159263611 export_path = /root/saved_model/1 Saved model success
- 查看Tensorboard。需要通过Kubectl的port-forward代理到Tensorboard Service。具体操作如下所示:
- 执行以下命令,获取Tensorboard Service的地址。
kubectl get svc -n demo-ns
预期输出:NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE tf-dist-arena-tensorboard NodePort 172.16.XX.XX <none> 6006:32226/TCP 80m
- 执行以下命令,通过Kubectl代理Tensorboard端口。
kubectl port-forward svc/tf-dist-arena-tensorboard -n demo-ns 6006:6006
预期输出:Forwarding from 127.0.0.1:6006 -> 6006 Forwarding from [::1]:6006 -> 6006 Handling connection for 6006 Handling connection for 6006
- 在浏览器地址栏输入
http://localhost:6006/
,即可查看Tensorboard。
- 执行以下命令,获取Tensorboard Service的地址。
示例二:提交Tensorflow分布式训练任务
方式一:通过Arena命令行提交训练任务
- 执行以下命令,通过Arena命令行提交训练任务。
arena submit tf \ -n demo-ns \ --name=tf-dist-arena \ --working-dir=/root/ \ --data fashion-mnist-pvc:/data \ --env=TEST_TMPDIR=/ \ --env=GIT_SYNC_USERNAME=kubeai \ --env=GIT_SYNC_PASSWORD=kubeai@ACK123 \ --env=GIT_SYNC_BRANCH=master \ --gpus=1 \ --workers=2 \ --worker-image=tensorflow/tensorflow:1.5.0-devel-gpu \ --sync-mode=git \ --sync-source=https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git \ --ps=1 \ --ps-image=tensorflow/tensorflow:1.5.0-devel \ --tensorboard \ "python code/tensorflow-fashion-mnist-sample/tf-distributed-mnist.py --log_dir=/training_logs"
- 执行以下命令,获取Tensorboard Service的地址。
kubectl get svc -n demo-ns
预期输出:NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE tf-dist-arena-tensorboard NodePort 172.16.204.248 <none> 6006:32226/TCP 80m
- 执行以下命令,通过Kubectl代理Tensorboard的库。查看Tensorboard需要通过Kubectl的port-forward代理到Tensorboard Service。
kubectl port-forward svc/tf-dist-arena-tensorboard -n demo-ns 6006:6006
预期输出:Forwarding from 127.0.0.1:6006 -> 6006 Forwarding from [::1]:6006 -> 6006 Handling connection for 6006 Handling connection for 6006
- 在浏览器地址栏输入
http://localhost:6006/
即可查看Tensorboard。
方式二:通过AI开发控制台提交训练任务
- 配置数据源。具体操作,请参见配置训练数据。本示例复用1的数据,不再重复配置。
- 配置代码源。具体操作,请参见配置训练代码。本示例复用2的代码,不再重复配置。
- 提交Tensorflow分布式训练任务。具体操作,请参见提交Tensorflow训练任务。配置训练参数后,单击提交,然后即可在任务列表查看到训练任务。提交Job的参数如下所示:
参数名 说明 任务名称 本示例为fashion-ps-ui。 任务类型 本示例选择TF分布式。 命名空间 本示例为demo-ns,与数据集所在命名空间必须相同。 数据源配置 本示例为fashion-demo,选择步骤1中的配置。 代码配置 本示例为fashion-git,选择步骤2中的配置。 执行命令 本示例为 "export TEST_TMPDIR=/root/ && python code/tensorflow-fashion-mnist-sample/tf-distributed-mnist.py --log_dir=/training_logs"
。镜像 - 任务资源配置下Worker页签的镜像,本示例配置为
tensorflow/tensorflow:1.5.0-devel-gpu
。 - 任务资源配置下的PS页签的镜像,本示例配置为
tensorflow/tensorflow:1.5.0-devel
。
关于更多的Arena命令行参数,请参见Arena提交TFJob。
- 任务资源配置下Worker页签的镜像,本示例配置为
- 查看Tensorboard。具体操作,请参见方式一:通过Arena命令行提交训练任务的2至4。
示例三:提交Fluid加速训练任务
- 管理员在运维控制台,一键加速现有数据集。
- 开发者使用Arena提交使用加速数据集的任务。
- Arena list对比Job运行时长。
- 一键加速现有数据集。若您在步骤二:创建数据集中已一键加速fashion-demo-pvc,则可跳过此步骤。关于如何创建加速数据集,请参见基于OSS创建加速数据集。
- 提交使用加速数据集的任务。开发者在命名空间demo-ns下,提交使用加速数据集的训练任务。加速数据集的任务与不使用加速数据集的任务主要区别在于:
--data
:加速后的PVC名称,本示例为fashion-demo-pvc-acc
。--env=DATASET_PATH
:代码中读取数据的路径,为Mount Path(本示例为--data
中的/root/data/)+ PVC名称(本示例为fashion-demo-pvc-acc)。
arena \ submit \ tfjob \ -n demo-ns \ --name=fashion-mnist-fluid \ --data=fashion-demo-pvc-acc:/root/data/ \ --env=DATASET_PATH=/root/data/fashion-demo-pvc-acc \ --env=MODEL_PATH=/root/saved_model \ --env=MODEL_VERSION=1 \ --env=GIT_SYNC_USERNAME=${GIT_USERNAME} \ --env=GIT_SYNC_PASSWORD=${GIT_PASSWORD} \ --sync-mode=git \ --sync-source=https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git \ --image="tensorflow/tensorflow:2.2.2-gpu" \ "python /root/code/tensorflow-fashion-mnist-sample/train.py --log_dir=/training_logs"
- 执行以下命令对比两次训练任务的执行速度。
arena list -n demo-ns
预期输出:
NAME STATUS TRAINER DURATION GPU(Requested) GPU(Allocated) NODE fashion-mnist-fluid SUCCEEDED TFJOB 33s 0 N/A 192.168.5.7 fashion-mnist-arena SUCCEEDED TFJOB 3m 0 N/A 192.168.5.8
通过Arena List两次训练结果可以看出,在相同的训练代码和节点资源下,使用Fluid加速之后的训练任务耗时33秒,不采用加速的训练任务耗时3分钟。
示例四:使用ACK AI任务调度器加速分布式训练任务
ACK AI任务调度器是阿里云ACK为云原生AI和大数据定制的优化调度器插件,支持Gang Scheduling,Capacity Scheduling和拓扑感知调度等。这里以GPU的拓扑感知调度为例,查看其加速效果。
ACK AI任务调度器基于节点异构资源的拓扑信息,例如GPU卡之间的Nvlink、PcleSwitch等通信方式,或者CPU的NUMA拓扑结构,在集群维度进行最佳的调度选择,提供给AI作业更好的性能。关于GPU拓扑感知调度的更多信息,请参见GPU拓扑感知调度概述。关于CPU拓扑感知调度的更多信息,请参见CPU拓扑感知调度。
根据以下示例介绍如何开启GPU拓扑感知调度,并对比加速效果。
- 执行以下命令,创建不开启拓扑感知调度的训练任务。
arena submit mpi \ --name=tensorflow-4-vgg16 \ --gpus=1 \ --workers=4 \ --image=registry.cn-hangzhou.aliyuncs.com/kubernetes-image-hub/tensorflow-benchmark:tf2.3.0-py3.7-cuda10.1 \ "mpirun --allow-run-as-root -np "4" -bind-to none -map-by slot -x NCCL_DEBUG=INFO -x NCCL_SOCKET_IFNAME=eth0 -x LD_LIBRARY_PATH -x PATH --mca pml ob1 --mca btl_tcp_if_include eth0 --mca oob_tcp_if_include eth0 --mca orte_keep_fqdn_hostnames t --mca btl ^openib python /tensorflow/benchmarks/scripts/tf_cnn_benchmarks/tf_cnn_benchmarks.py --model=vgg16 --batch_size=64 --variable_update=horovod"
- 创建开启拓扑感知调度的训练任务。为节点打标签,下面以cn-beijing.192.168.XX.XX的节点为例,实际操作时,需要替换为您的集群节点名称。
kubectl label node cn-beijing.192.168.XX.XX ack.node.gpu.schedule=topology --overwrite
执行以下命令,创建开启拓扑感知调度的训练任务,并打开Arena拓扑感知开关--gputopology=true
。arena submit mpi \ --name=tensorflow-topo-4-vgg16 \ --gpus=1 \ --workers=4 \ --gputopology=true \ --image=registry.cn-hangzhou.aliyuncs.com/kubernetes-image-hub/tensorflow-benchmark:tf2.3.0-py3.7-cuda10.1 \ "mpirun --allow-run-as-root -np "4" -bind-to none -map-by slot -x NCCL_DEBUG=INFO -x NCCL_SOCKET_IFNAME=eth0 -x LD_LIBRARY_PATH -x PATH --mca pml ob1 --mca btl_tcp_if_include eth0 --mca oob_tcp_if_include eth0 --mca orte_keep_fqdn_hostnames t --mca btl ^openib python /tensorflow/benchmarks/scripts/tf_cnn_benchmarks/tf_cnn_benchmarks.py --model=vgg16 --batch_size=64 --variable_update=horovod
- 对比两次训练任务的执行速度。
- 执行以下命令,对比两次训练任务的执行速度。
arena list -n demo-ns
预期输出:
NAME STATUS TRAINER DURATION GPU(Requested) GPU(Allocated) NODE tensorflow-topo-4-vgg16 SUCCEEDED MPIJOB 44s 4 N/A 192.168.4.XX1 tensorflow-4-vgg16-image-warned SUCCEEDED MPIJOB 2m 4 N/A 192.168.4.XX0
- 执行以下命令,查看未开启拓扑感知调度的训练任务的总处理速度。
arena logs tensorflow-topo-4-vgg16 -n demo-ns
预期输出:100 images/sec: 251.7 +/- 0.1 (jitter = 1.2) 7.262 ---------------------------------------------------------------- total images/sec: 1006.44
- 执行以下命令,查看开启拓扑感知调度的训练任务的总处理速度。
arena logs tensorflow-4-vgg16-image-warned -n demo-ns
预期输出:100 images/sec: +/- 0.2 (jitter = 1.5) 7.261 ---------------------------------------------------------------- total images/sec: 225.50
- 执行以下命令,对比两次训练任务的执行速度。
训练任务 | 单卡处理速度(ns) | 总处理速度(ns) | 运行时长(秒) |
开启拓扑感知调度 | 56.4 | 225.50 | 44 |
未开启拓扑感知调度 | 251.7 | 1006.44 | 120 |
kubectl label node cn-beijing.192.168.XX.XX0 ack.node.gpu.schedule=default --overwrite
步骤五:模型管理
- 访问AI开发控制台。
- 在AI开发控制台的左侧导航栏中,单击模型管理。
- 单击模型管理页面的创建模型。
- 在创建对话框中,配置需要创建的模型名称、模型版本以及该模型对应关联的训练的Job。本示例的模型名称为fsahion-mnist-demo,模型版本为v1,训练的Job选择为tf-single。
- 单击确定后,即可看到新增的模型。
如果需要评测新增的模型,可在相应的模型后单击新增评测。
步骤六:模型评测
- 通过Arena提交开启Checkpoint的训练任务。
- 通过Arena提交评测任务。
- 通过AI开发控制台对比不同评测的效果。
- 提交开启Checkpoint的训练任务。执行以下命令,通过Arena提交开启输出Checkpoint的训练任务,并保存Checkpoint到fashion-demo-pvc中。
arena \ submit \ tfjob \ -n demo-ns \ #您可根据需要配置命名空间。 --name=fashion-mnist-arena-ckpt \ --data=fashion-demo-pvc:/root/data/ \ --env=DATASET_PATH=/root/data/ \ --env=MODEL_PATH=/root/data/saved_model \ --env=MODEL_VERSION=1 \ --env=GIT_SYNC_USERNAME=${GIT_USERNAME} \ #输入您的Git用户名。 --env=GIT_SYNC_PASSWORD=${GIT_PASSWORD} \ #输入您的Git密码。 --env=OUTPUT_CHECKPOINT=1 \ --sync-mode=git \ --sync-source=https://codeup.aliyun.com/60b4cf5c66bba1c04b442e49/tensorflow-fashion-mnist-sample.git \ --image="tensorflow/tensorflow:2.2.2-gpu" \ "python /root/code/tensorflow-fashion-mnist-sample/train.py --log_dir=/training_logs"
- 提交评测任务。
- 制作评测镜像。获取模型评估代码,在kubeai-sdk目录执行以下命令,构建并推送镜像。
docker build . -t ${DOCKER_REGISTRY}:fashion-mnist docker push ${DOCKER_REGISTRY}:fashion-mnist
- 执行以下命令,获取AI套件部署的Mysql。
kubectl get svc -n kube-ai ack-mysql
预期输出:NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ack-mysql ClusterIP 172.16.XX.XX <none> 3306/TCP 28h
- 执行以下命令,通过Arena提交评测任务。
arena evaluate model \ --namespace=demo-ns \ --loglevel=debug \ --name=evaluate-job \ --image=registry.cn-beijing.aliyuncs.com/kube-ai/kubeai-sdk-demo:fashion-minist \ --env=ENABLE_MYSQL=True \ --env=MYSQL_HOST=172.16.77.227 \ --env=MYSQL_PORT=3306 \ --env=MYSQL_USERNAME=kubeai \ --env=MYSQL_PASSWORD=kubeai@ACK \ --data=fashion-demo-pvc:/data \ --model-name=1 \ --model-path=/data/saved_model/ \ --dataset-path=/data/ \ --metrics-path=/data/output \ "python /kubeai/evaluate.py"
说明 MYSQL的IP地址和端口可从上一步获取。
- 制作评测镜像。
- 对比评测结果。
- 在AI开发控制台的左侧导航栏中,单击模型管理。
- 在任务列表中单击目标评测任务的名称,即可看到对应评测任务的指标。也可选择多个任务对比指标:
步骤七:模型部署
模型开发、评测完成之后,需要发布为服务,供业务系统调用。以下内容介绍如何将上述步骤生成的模型发布为tf-serving服务。Arena同时支持Triton及Seldon等服务框架。更多信息,请参见Arena serve文档。
以下示例使用步骤四:模型训练训练产出的模型,并将模型存储在步骤二:创建数据集的PVC下(即fashion-minist-demo PVC)。若您的模型来自其他存储类型,则需要先在集群中创建相应的PVC。
- 执行以下命令,通过Arena将Tensorflow模型部署到Tensorflow serving上。
arena serve tensorflow \ --loglevel=debug \ --namespace=demo-ns \ --name=fashion-mnist \ --model-name=1 \ --gpus=1 \ --image=tensorflow/serving:1.15.0-gpu \ --data=fashion-demo-pvc:/data \ --model-path=/data/saved_model/ \ --version-policy=latest
- 执行以下命令,获取部署的推理服务名。
arena serve list -n demo-ns
预期输出:
NAME TYPE VERSION DESIRED AVAILABLE ADDRESS PORTS GPU fashion-mnist Tensorflow 202111031203 1 1 172.16.XX.XX GRPC:8500,RESTFUL:8501 1
预期输出中的ADDRESS和PORTS可用于集群内调用。
- 在Jupyter中新建Jupyter Notebook文件,作为请求tf-serving HTTP协议服务的Client。本示例使用步骤三:模型开发中创建的Jupyter Notebook来发起请求。
- 本示例初始化代码中的
server_ip
替换为上一步获取的ADDRESS(172.16.XX.XX)。 - 本示例初始化代码中的
server_http_port
为上一步中获取的RESTFUL端口(8501)。
Notebook文件的初始化代码如下所示:
import os import gzip import numpy as np # import matplotlib.pyplot as plt import random import requests import json server_ip = "172.16.XX.XX" server_http_port = 8501 dataset_dir = "/root/data/" def load_data(): files = [ 'train-labels-idx1-ubyte.gz', 'train-images-idx3-ubyte.gz', 't10k-labels-idx1-ubyte.gz', 't10k-images-idx3-ubyte.gz' ] paths = [] for fname in files: paths.append(os.path.join(dataset_dir, fname)) with gzip.open(paths[0], 'rb') as labelpath: y_train = np.frombuffer(labelpath.read(), np.uint8, offset=8) with gzip.open(paths[1], 'rb') as imgpath: x_train = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(y_train), 28, 28) with gzip.open(paths[2], 'rb') as labelpath: y_test = np.frombuffer(labelpath.read(), np.uint8, offset=8) with gzip.open(paths[3], 'rb') as imgpath: x_test = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(y_test), 28, 28) return (x_train, y_train),(x_test, y_test) def show(idx, title): plt.figure() plt.imshow(test_images[idx].reshape(28,28)) plt.axis('off') plt.title('\n\n{}'.format(title), fontdict={'size': 16}) class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] (train_images, train_labels), (test_images, test_labels) = load_data() train_images = train_images / 255.0 test_images = test_images / 255.0 # reshape for feeding into the model train_images = train_images.reshape(train_images.shape[0], 28, 28, 1) test_images = test_images.reshape(test_images.shape[0], 28, 28, 1) print('\ntrain_images.shape: {}, of {}'.format(train_images.shape, train_images.dtype)) print('test_images.shape: {}, of {}'.format(test_images.shape, test_images.dtype)) rando = random.randint(0,len(test_images)-1) #show(rando, 'An Example Image: {}'.format(class_names[test_labels[rando]])) # !pip install -q requests # import requests # headers = {"content-type": "application/json"} # json_response = requests.post('http://localhost:8501/v1/models/fashion_model:predict', data=data, headers=headers) # predictions = json.loads(json_response.text)['predictions'] # show(0, 'The model thought this was a {} (class {}), and it was actually a {} (class {})'.format( # class_names[np.argmax(predictions[0])], np.argmax(predictions[0]), class_names[test_labels[0]], test_labels[0])) def request_model(data): headers = {"content-type": "application/json"} json_response = requests.post('http://{}:{}/v1/models/1:predict'.format(server_ip, server_http_port), data=data, headers=headers) print('=======response:', json_response, json_response.text) predictions = json.loads(json_response.text)['predictions'] print('The model thought this was a {} (class {}), and it was actually a {} (class {})'.format(class_names[np.argmax(predictions[0])], np.argmax(predictions[0]), class_names[test_labels[0]], test_labels[0])) #show(0, 'The model thought this was a {} (class {}), and it was actually a {} (class {})'.format( # class_names[np.argmax(predictions[0])], np.argmax(predictions[0]), class_names[test_labels[0]], test_labels[0])) # def request_model_version(data): # headers = {"content-type": "application/json"} # json_response = requests.post('http://{}:{}/v1/models/1/version/1:predict'.format(server_ip, server_http_port), data=data, headers=headers) # print('=======response:', json_response, json_response.text) # predictions = json.loads(json_response.text) # for i in range(0,3): # show(i, 'The model thought this was a {} (class {}), and it was actually a {} (class {})'.format( # class_names[np.argmax(predictions[i])], np.argmax(predictions[i]), class_names[test_labels[i]], test_labels[i])) data = json.dumps({"signature_name": "serving_default", "instances": test_images[0:3].tolist()}) print('Data: {} ... {}'.format(data[:50], data[len(data)-52:])) #request_model_version(data) request_model(data)
单击Jupyter Notebook的图标,即可看到以下执行结果:train_images.shape: (60000, 28, 28, 1), of float64 test_images.shape: (10000, 28, 28, 1), of float64 Data: {"signature_name": "serving_default", "instances": ... [0.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0]]]]} =======response: <Response [200]> { "predictions": [[7.42696e-07, 6.91237556e-09, 2.66364452e-07, 2.27735413e-07, 4.0373439e-07, 0.00490919966, 7.27086217e-06, 0.0316713452, 0.0010733594, 0.962337255], [0.00685342, 1.8516447e-08, 0.9266119, 2.42278338e-06, 0.0603800081, 4.01338771e-12, 0.00613868702, 4.26091073e-15, 1.35764185e-05, 3.38685469e-10], [1.09047969e-05, 0.999816835, 7.98738e-09, 0.000122893631, 4.85748023e-05, 1.50353979e-10, 3.57102294e-07, 1.89657579e-09, 4.4604468e-07, 9.23274524e-09] ] } The model thought this was a Ankle boot (class 9), and it was actually a Ankle boot (class 9)
- 本示例初始化代码中的
常见问题
- 如何在Jupyter Notebook控制台安装常用软件?答:可通过执行以下命令在Jupyter Notebook控制台安装软件。
apt-get install ${您需要的软件}
- 如何解决Jupyter Notebook控制台字符集乱码问题?答:根据以下示例编辑/etc/locale文件后,重新打开Terminal。
LC_CTYPE="da_DK.UTF-8" LC_NUMERIC="da_DK.UTF-8" LC_TIME="da_DK.UTF-8" LC_COLLATE="da_DK.UTF-8" LC_MONETARY="da_DK.UTF-8" LC_MESSAGES="da_DK.UTF-8" LC_PAPER="da_DK.UTF-8" LC_NAME="da_DK.UTF-8" LC_ADDRESS="da_DK.UTF-8" LC_TELEPHONE="da_DK.UTF-8" LC_MEASUREMENT="da_DK.UTF-8" LC_IDENTIFICATION="da_DK.UTF-8" LC_ALL=