Tous les produits
Search
Centre de documentation

Platform For AI:Train and deploy a PyTorch model

Dernière mise à jour :Aug 09, 2026

Le SDK PAI pour Python propose des API de haut niveau pour l'entraînement et le déploiement de modèles sur Platform for AI (PAI). Ce tutoriel vous guide pas à pas dans l'entraînement d'un modèle de classification d'images avec PyTorch sur le jeu de données MNIST, puis dans son déploiement en tant que service d'inférence en ligne sur Elastic Algorithm Service (EAS).

Le processus se déroule en cinq étapes :

  1. Installez et configurez le SDK PAI pour Python

  2. Chargez les données d'entraînement vers Object Storage Service (OSS)

  3. Rédigez un script d'entraînement adapté à PAI

  4. Soumettez une tâche d'entraînement

  5. Déployez un service d'inférence sur EAS

Un notebook Jupyter correspondant à ce tutoriel est disponible en téléchargement : pytorch_mnist.ipynb

Prérequis

Avant de commencer, assurez-vous de disposer des éléments suivants :

Étape 1 : Installer et configurer le SDK PAI pour Python

Installez le SDK :

python -m pip install "alipai>=0.4.0"
Si vous obtenez une erreur ModuleNotFoundError , exécutez d'abord la commande pip install --upgrade pip .

Configurez le SDK avec votre paire AccessKey, votre espace de travail et votre compartiment OSS :

python -m pai.toolkit.config

Pour plus de détails sur la configuration, consultez la rubrique Installer et configurer le SDK PAI pour Python.

Étape 2 : Charger les données d'entraînement vers OSS

Ce tutoriel utilise le jeu de données MNIST (chiffres manuscrits) pour entraîner un modèle de classification d'images. Les tâches d'entraînement PAI lisent les données d'entrée depuis OSS ; vous devez donc charger le jeu de données avant de soumettre la tâche.

Télécharger le jeu de données MNIST

Exécutez le script shell suivant pour télécharger le jeu de données dans un répertoire local data :

#!/bin/sh
set -e

url_prefix="https://ossci-datasets.s3.amazonaws.com/mnist/"
# Alternative mirror if the download is slow:
# url_prefix="http://yann.lecun.com/exdb/mnist/"

mkdir -p data/MNIST/raw/

wget -nv ${url_prefix}train-images-idx3-ubyte.gz -P data/MNIST/raw/
wget -nv ${url_prefix}train-labels-idx1-ubyte.gz -P data/MNIST/raw/
wget -nv ${url_prefix}t10k-images-idx3-ubyte.gz  -P data/MNIST/raw/
wget -nv ${url_prefix}t10k-labels-idx1-ubyte.gz  -P data/MNIST/raw/

Charger vers OSS

Utilisez ossutil ou le SDK PAI pour Python :

  • Avec ossutil (consultez la rubrique ossutil 1.0) :

    ossutil cp -rf ./data oss://<YourOssBucket>/mnist/data/
  • Avec le SDK PAI pour Python :

    from pai.common.oss_utils import upload
    from pai.session import get_default_session
    
    sess = get_default_session()
    data_uri = upload("./data/", oss_path="mnist/data/", bucket=sess.oss_bucket)
    print(data_uri)
Si vous effectuez le chargement avec ossutil , définissez explicitement data_uri = "oss://<YourOssBucket>/mnist/data/" lors de la soumission de la tâche d'entraînement à l'étape 4.

Étape 3 : Rédiger un script d'entraînement

PAI monte les données d'entrée et capture la sortie du modèle via des variables d'environnement. Votre script d'entraînement doit lire ces variables au lieu d'utiliser des chemins codés en dur.

Variables d'environnement PAI pour les tâches d'entraînement

Variable Description
PAI_INPUT_<CHANNEL_NAME> Chemin de montage pour un canal de données d'entrée. Le nom du canal est converti en majuscules. Par exemple, un canal nommé train_data correspond à PAI_INPUT_TRAIN_DATA.
PAI_OUTPUT_MODEL Chemin où PAI écrit la sortie du modèle. Valeur par défaut : /ml/output/model. Les fichiers écrits ici sont automatiquement sauvegardés dans votre compartiment OSS.
PAI_USER_ARGS Hyperparamètres sous forme de chaîne d'arguments CLI (par exemple, --epochs 5 --batch-size 256). Utilisé dans le champ command de l'estimateur.
PAI_CONFIG_DIR Répertoire contenant le fichier hyperparameters.json avec les mêmes valeurs d'hyperparamètres.

Ce tutoriel part de l'exemple MNIST du dépôt PyTorch et y apporte deux modifications :

Modification 1 — Lire les données d'entrée depuis la variable d'environnement

- dataset1 = datasets.MNIST("../data", train=True, download=True, transform=transform)
- dataset2 = datasets.MNIST("../data", train=False, transform=transform)

+ # Read the mounted data path from the environment variable.
+ data_path = os.environ.get("PAI_INPUT_TRAIN_DATA", "../data")
+ dataset1 = datasets.MNIST(data_path, train=True, download=True, transform=transform)
+ dataset2 = datasets.MNIST(data_path, train=False, transform=transform)

Modification 2 — Enregistrer le modèle au format TorchScript dans le chemin de sortie

Le processeur PyTorch intégré requiert des modèles au format TorchScript.

- if args.save_model:
-     torch.save(model.state_dict(), "mnist_cnn.pt")

+ # Save the model.
+ save_model(model)

+ def save_model(model):
+     """Convert the model to TorchScript and save it to the output path."""
+     output_model_path = os.environ.get("PAI_OUTPUT_MODEL")
+     os.makedirs(output_model_path, exist_ok=True)
+
+     m = torch.jit.script(model)
+     m.save(os.path.join(output_model_path, "mnist_cnn.pt"))

Script d'entraînement complet

Enregistrez le script suivant dans train_src/train.py :

# source: https://github.com/pytorch/examples/blob/main/mnist/main.py
from __future__ import print_function

import argparse
import os

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
from torchvision import datasets, transforms

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output

def train(args, model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print(
                "Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format(
                    epoch,
                    batch_idx * len(data),
                    len(train_loader.dataset),
                    100.0 * batch_idx / len(train_loader),
                    loss.item(),
                )
            )
            if args.dry_run:
                break

def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(
                output, target, reduction="sum"
            ).item()  # sum up batch loss
            pred = output.argmax(
                dim=1, keepdim=True
            )  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

    print(
        "\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n".format(
            test_loss,
            correct,
            len(test_loader.dataset),
            100.0 * correct / len(test_loader.dataset),
        )
    )

def main():
    # Training settings
    parser = argparse.ArgumentParser(description="PyTorch MNIST Example")
    parser.add_argument(
        "--batch-size",
        type=int,
        default=64,
        metavar="N",
        help="input batch size for training (default: 64)",
    )
    parser.add_argument(
        "--test-batch-size",
        type=int,
        default=1000,
        metavar="N",
        help="input batch size for testing (default: 1000)",
    )
    parser.add_argument(
        "--epochs",
        type=int,
        default=14,
        metavar="N",
        help="number of epochs to train (default: 14)",
    )
    parser.add_argument(
        "--lr",
        type=float,
        default=1.0,
        metavar="LR",
        help="learning rate (default: 1.0)",
    )
    parser.add_argument(
        "--gamma",
        type=float,
        default=0.7,
        metavar="M",
        help="Learning rate step gamma (default: 0.7)",
    )
    parser.add_argument(
        "--no-cuda", action="store_true", default=False, help="disables CUDA training"
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        default=False,
        help="quickly check a single pass",
    )
    parser.add_argument(
        "--seed", type=int, default=1, metavar="S", help="random seed (default: 1)"
    )
    parser.add_argument(
        "--log-interval",
        type=int,
        default=10,
        metavar="N",
        help="how many batches to wait before logging training status",
    )
    parser.add_argument(
        "--save-model",
        action="store_true",
        default=False,
        help="For Saving the current Model",
    )
    args = parser.parse_args()
    use_cuda = not args.no_cuda and torch.cuda.is_available()

    torch.manual_seed(args.seed)

    device = torch.device("cuda" if use_cuda else "cpu")

    train_kwargs = {"batch_size": args.batch_size}
    test_kwargs = {"batch_size": args.test_batch_size}
    if use_cuda:
        cuda_kwargs = {"num_workers": 1, "pin_memory": True, "shuffle": True}
        train_kwargs.update(cuda_kwargs)
        test_kwargs.update(cuda_kwargs)

    transform = transforms.Compose(
        [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]
    )

    data_path = os.environ.get("PAI_INPUT_TRAIN_DATA", "../data")
    dataset1 = datasets.MNIST(data_path, train=True, download=True, transform=transform)
    dataset2 = datasets.MNIST(data_path, train=False, transform=transform)
    train_loader = torch.utils.data.DataLoader(dataset1, **train_kwargs)
    test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)

    model = Net().to(device)
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)

    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)
    for epoch in range(1, args.epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
        scheduler.step()

    # Save the model.
    save_model(model)

def save_model(model):
    """Convert the model to TorchScript and save it to the specified path."""
    output_model_path = os.environ.get("PAI_OUTPUT_MODEL")
    os.makedirs(output_model_path, exist_ok=True)

    m = torch.jit.script(model)
    m.save(os.path.join(output_model_path, "mnist_cnn.pt"))

if __name__ == "__main__":
    main()

Structure de répertoire attendue pour le code d'entraînement :

|-- train_src/           # Training script directory (uploaded to OSS before the job starts)
    |-- requirements.txt # Optional: third-party dependencies
    '-- train.py         # Training script

Étape 4 : Soumettre une tâche d'entraînement

L'objet Estimator exécute votre script d'entraînement local sur une instance gérée par PAI, en utilisant une image de conteneur spécifiée.

Paramètres clés de l'estimateur

Paramètre Description
source_dir Répertoire de scripts local à charger. PAI le monte sur /ml/usercode (répertoire de travail pour command).
command Commande de démarrage. $PAI_USER_ARGS est développé en hyperparamètres sous forme d'indicateurs CLI — dans cet exemple, --epochs 5 --batch-size 256 --lr 0.5.
image_uri Image de conteneur. Utilisez retrieve() pour obtenir une image PyTorch gérée par PAI.
instance_type Instance de calcul. Pour les types pris en charge et la tarification, consultez la rubrique Détails de tarification du groupe de ressources public.
metric_definitions Modèles regex pour extraire les métriques d'entraînement des journaux stdout/stderr. PAI affiche les métriques analysées sur la page de détails de la tâche.
from pai.estimator import Estimator
from pai.image import retrieve

# Get a PAI-managed PyTorch 1.8 GPU image.
image_uri = retrieve(
    "PyTorch", framework_version="1.8PAI", accelerator_type="GPU"
).image_uri
print(image_uri)

est = Estimator(
    # Startup command; working directory is /ml/usercode.
    command="python train.py $PAI_USER_ARGS",
    # Local training script directory to upload.
    source_dir="./train_src/",
    image_uri=image_uri,
    # Instance type: 4 vCPU, 15 GB RAM, 1x NVIDIA T4.
    instance_type="ecs.gn6i-c4g1.xlarge",
    # Hyperparameters are passed to the script as CLI flags via $PAI_USER_ARGS.
    hyperparameters={
        "epochs": 5,
        "batch-size": 64 * 4,
        "lr": 0.5,
    },
    # Extract the loss metric from training logs.
    metric_definitions=[
        {
            "Name": "loss",
            "Regex": r".*loss=([-+]?[0-9]*.?[0-9]+(?:[eE][-+]?[0-9]+)?).*",
        },
    ],
    base_job_name="pytorch_mnist",
)

Soumettez la tâche et attendez sa fin d'exécution :

# If you uploaded with ossutil, set data_uri explicitly:
# data_uri = "oss://<YourOssBucket>/mnist/data/"

est.fit(
    inputs={
        "train_data": data_uri,
    }
)

# OSS path of the trained model.
print("Training job output model path:")
print(est.model_data())

Après la soumission, le SDK affiche un lien vers la page de détails de la tâche et diffuse les journaux d'entraînement jusqu'à la fin de celle-ci. Pour plus d'informations, consultez la rubrique Soumettre une tâche d'entraînement.

Étape 5 : Déployer un service d'inférence

Une fois la tâche d'entraînement terminée, utilisez est.model_data() pour obtenir le chemin OSS du modèle entraîné, puis déployez-le en tant que service d'inférence en ligne sur EAS.

Le déploiement d'un service nécessite deux composants :

  • Ressources de modèle : les fichiers de modèle entraînés issus d'OSS.

  • Environnement d'exécution : instructions pour charger le modèle et servir les prédictions — soit un processeur intégré, soit une image de conteneur personnalisée.

Choisissez une méthode de déploiement selon vos besoins :

Méthode Cas d'utilisation
Processeur intégré Formats de modèle standard (comme TorchScript). Aucun code de service personnalisé n'est requis.
Image personnalisée Modèles avec des dépendances tierces, ou lorsque vous avez besoin d'une logique de prétraitement et de post-traitement personnalisée.

Déployer avec un processeur intégré

PAI fournit un processeur PyTorch intégré qui charge et sert les modèles TorchScript. Utilisez cette méthode si votre modèle n'a pas d'exigences de service personnalisées.

Déployer le service

from pai.model import Model, InferenceSpec
from pai.predictor import Predictor
from pai.common.utils import random_str

m = Model(
    model_data=est.model_data(),
    # Use the built-in PyTorch processor.
    inference_spec=InferenceSpec(processor="pytorch_cpu_1.10"),
)

p: Predictor = m.deploy(
    service_name="tutorial_pt_mnist_proc_{}".format(random_str(6)),
    instance_type="ecs.c6.xlarge",
)

print(p.service_name)
print(p.service_status)
Le déploiement prend généralement plusieurs minutes. Le SDK bloque l'exécution jusqu'à ce que le service soit prêt.

La méthode Model.deploy renvoie un objet Predictor. Utilisez Predictor.predict pour envoyer des requêtes et obtenir des prédictions.

Exécuter l'inférence

import numpy as np

# Input shape: (batch_size, channels, height, width) as float32.
dummy_input = np.random.rand(2, 1, 28, 28).astype(np.float32)

res = p.predict(dummy_input)
print(res)
print(np.argmax(res, 1))

Supprimer le service

p.delete_service()

Déployer avec une image personnalisée

Utilisez cette méthode si votre modèle nécessite un prétraitement ou un post-traitement personnalisé, ou s'il comporte des dépendances non couvertes par le processeur intégré.

Étape 1 : Rédiger le code du service d'inférence

Créez le fichier infer_src/run.py avec une application Flask qui charge le modèle et gère les requêtes HTTP :

import json
from flask import Flask, request
from PIL import Image
import os
import torch
import torchvision.transforms as transforms
import numpy as np
import io

app = Flask(__name__)
# The model is loaded from this path by default.
MODEL_PATH = "/eas/workspace/model/"

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = torch.jit.load(os.path.join(MODEL_PATH, "mnist_cnn.pt"), map_location=device).to(device)
transform = transforms.Compose(
    [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]
)

@app.route("/", methods=["POST"])
def predict():
    # Preprocess the image.
    im = Image.open(io.BytesIO(request.data))
    input_tensor = transform(im).to(device)
    input_tensor.unsqueeze_(0)
    # Run inference.
    output_tensor = model(input_tensor)
    pred_res = output_tensor.detach().cpu().numpy()[0]
    return json.dumps(pred_res.tolist())

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=int(os.environ.get("LISTENING_PORT", 8000)))

Structure de répertoire attendue :

|-- infer_src/           # Inference service code directory (uploaded to OSS before deployment)
    |-- requirements.txt # Optional: third-party dependencies
    '-- run.py           # Inference server script

Étape 2 : Construire l'InferenceSpec

La fonction container_serving_spec regroupe votre code local avec une image PyTorch gérée par PAI. Le répertoire source_dir est chargé vers OSS et monté sur /ml/usercode dans le conteneur.

from pai.model import InferenceSpec, container_serving_spec
from pai.image import retrieve, ImageScope

torch_image_uri = retrieve(
    "PyTorch", framework_version="latest", image_scope=ImageScope.INFERENCE
).image_uri

inf_spec = container_serving_spec(
    command="python run.py",
    source_dir="./infer_src/",
    image_uri=torch_image_uri,
    requirements=["flask==2.0.0", "Werkzeug==2.2.2", "pillow", "torchvision"],
)
print(inf_spec.to_dict())

Étape 3 : Déployer le service

from pai.model import Model
from pai.common.utils import random_str

m = Model(
    model_data=est.model_data(),
    inference_spec=inf_spec,
)

predictor = m.deploy(
    service_name="torch_mnist_script_container_{}".format(random_str(6)),
    instance_type="ecs.c6.xlarge",
)
Le déploiement prend généralement plusieurs minutes. Le SDK bloque l'exécution jusqu'à ce que le service soit prêt.

Étape 4 : Exécuter l'inférence

Préparez une image de test MNIST :

import base64
from PIL import Image
import io

# raw_data is an MNIST image of the digit 9.
raw_data = base64.b64decode(b"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAAcABwBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APn+rVhpmoarP5GnWNzeTYz5dvE0jfkoJovNMv8ATmK3tjc2zByhE8TIQw6jkdR6VVq9oumPrWuWGlxyLG95cRwK7dFLMFyfzr3aXwp4ltAfB3gWwudI01JNuoa7eZhku5AMHafvFOw2Dn6ZJ4z4yeLk1HUbXwrZSSy2Oh5heeaQu88wG1mLHk4wR9c+1eXUqsVYMpIIOQR2r1D4QazqOs/FnSG1fVLi9ZI5vL+2TNKc+U2ApYnB7/hXml5LLNfXEsxLSvIzOSMEsTk1DRVnT7+60vULe/spmhureQSRSL1Vh0NWNd1mXX9ZuNUuLe2gmuCGkS2QohbABbBJwTjJ9yelZ1f/2Q==")

im = Image.open(io.BytesIO(raw_data))

Envoyez l'image au service d'inférence. Le service accepte les octets d'image bruts dans le corps de la requête HTTP :

from pai.predictor import RawResponse
import numpy as np

resp: RawResponse = predictor.raw_predict(data=raw_data)
print(resp.json())
print(np.argmax(resp.json()))

Étape 5 : Supprimer le service

predictor.delete_service()

Étapes suivantes