Qu'est-ce qui est autorisé par Jupyter?

Notre histoire a commencé par une tâche apparemment simple. Il était nécessaire de mettre en place des outils d'analyse pour les spécialistes de la science des données et uniquement les analystes de données. Cette tâche nous a été confiée par des collègues des divisions Risques Retail et CRM, où la concentration de spécialistes en data science est historiquement élevée. Les clients avaient un simple désir: écrire du code Python, importer des bibliothèques avancées (xgboost, pytorch, tensorflow, etc.) et exécuter des algorithmes sur les données générées à partir du cluster hdfs.



Tout semble simple et clair. Mais il y avait tellement d'embûches que nous avons décidé d'écrire un article à ce sujet et de publier la solution prête à l'emploi sur GitHub.

Tout d'abord, quelques détails sur l'infrastructure source:

  • HDFS Data Warehouse (12 nœuds Oracle Big Data Appliance, distribution Cloudera). Au total, l'entrepôt dispose de 130 To de données provenant de divers systèmes internes de la banque; il existe également des informations hétérogènes provenant de sources externes.
  • Deux serveurs d'applications sur lesquels le déploiement d'outils analytiques était supposé. Il convient de mentionner que non seulement les tâches d'analyse avancées "tournent" sur ces serveurs, mais l'une des exigences était l'utilisation d'outils de conteneurisation (Docker) pour gérer les ressources du serveur, utiliser divers environnements et les configurer.

En tant qu'environnement principal pour le travail des analystes, ils ont décidé de choisir JupyterHub, qui de facto est déjà devenu l'un des standards pour travailler avec les données et développer des modèles d'apprentissage automatique. En savoir plus ici . À l'avenir, nous avons déjà imaginé JupyterLab.

Il semblerait que tout soit simple: vous devez prendre et configurer un tas de Python + Anaconda + Spark. Installez Jupyter Hub sur le serveur d'applications, intégrez avec LDAP, connectez Spark ou connectez-vous aux données dans hdfs de toute autre manière et allez-y - créez des modèles!
Si vous explorez toutes les données source et les exigences, voici une liste plus détaillée:

  • Exécution de JupyterHub dans Docker (système d'exploitation de base - Oracle Linux 7)
  • Cloudera CDH cluster 5.15.1 + Spark 2.3.0 avec authentification Kerberos dans la configuration Active Directory + Kerberos MIT dédié dans le cluster (voir Cluster MIT KDC dédié avec Active Directory ), Oracle Linux 6
  • Intégration Active Directory
  • Authentification transparente dans Hadoop et Spark
  • Prise en charge de Python 2 et 3
  • Spark 1 et 2 (avec la possibilité d'utiliser des ressources de cluster pour former des modèles et paralléliser le traitement des données à l'aide de pyspark)
  • Capacité à limiter les ressources de l'hôte
  • Ensemble de bibliothèque

Ce poste est conçu pour les professionnels de l'informatique confrontés à la nécessité de résoudre de tels problèmes.

Description de la solution


Lancement dans Docker + Cloudera Cluster Integration


Il n'y a rien d'inhabituel ici. Les clients des produits JupyterHub et Cloudera sont installés dans le conteneur (comme - voir ci-dessous), et les fichiers de configuration sont montés à partir de la machine hôte:

start-hub.sh

VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 


Intégration Active Directory


Pour l'intégration avec le fer Active Directory / Kerberos et pas très hôtes, la norme dans notre société est le produit PBIS Open . Techniquement, ce produit est un ensemble de services qui communiquent avec Active Directory, avec lequel, à leur tour, les clients travaillent via des sockets de domaine Unix. Ce produit s'intègre à Linux PAM et NSS.

Nous avons utilisé la méthode Docker standard - les sockets de domaine unix des services hôtes ont été montés dans un conteneur (les sockets ont été trouvés empiriquement par de simples manipulations avec la commande lsof):

start-hub.sh

 VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z <b>-v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro </b> -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

À leur tour, les packages PBIS sont installés à l'intérieur du conteneur, mais sans exécuter la section de post-installation. Nous ne mettons donc que des fichiers exécutables et des bibliothèques, mais ne démarrons pas de services à l'intérieur du conteneur - c'est superflu pour nous. Les commandes d'intégration PAM et NSS Linux sont exécutées manuellement.

Dockerfile:

 # Install PAM itself and standard PAM configuration packages. RUN yum install -y pam util-linux \ # Here we just download PBIS RPM packages then install them omitting scripts. # We don't need scripts since they start PBIS services, which are not used - we connect to the host services instead. && find /var/yum/localrepo/ -type f -name 'pbis-open*.rpm' | xargs rpm -ivh --noscripts \ # Enable PBIS PAM integration. && domainjoin-cli configure --enable pam \ # Make pam_loginuid.so module optional (Docker requirement) and add pam_mkhomedir.so to have home directories created automatically. && mv /etc/pam.d/login /tmp \ && awk '{ if ($1 == "session" && $2 == "required" && $3 == "pam_loginuid.so") { print "session optional pam_loginuid.so"; print "session required pam_mkhomedir.so skel=/etc/skel/ umask=0022";} else { print $0; } }' /tmp/login > /etc/pam.d/login \ && rm /tmp/login \ # Enable PBIS nss integration. && domainjoin-cli configure --enable nsswitch 

Il s'avère que les clients du conteneur PBIS communiquent avec les services hôtes PBIS. JupyterHub utilise un authentificateur PAM, et avec PBIS correctement configuré sur l'hôte, tout fonctionne hors de la boîte.

Afin d'empêcher tous les utilisateurs d'AD d'entrer dans JupyterHub, vous pouvez utiliser le paramètre qui restreint les utilisateurs à des groupes AD spécifiques.

exemple-config / jupyterhub / jupyterhub_config.py

 c.DSAIAuthenticator.group_whitelist = ['COMPANY\\domain^users'] 

Authentification transparente dans Hadoop et Spark


Lors de la connexion à JupyterHub, PBIS met en cache le ticket Kerberos de l'utilisateur dans un fichier spécifique du répertoire / tmp. Pour une authentification transparente de cette manière, il suffit de monter le répertoire host / tmp dans le conteneur et de définir la variable KRB5CCNAME à la valeur souhaitée (cela se fait dans notre classe d'authentificateur).

start-hub.sh

 VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

assets / jupyterhub / dsai.py

 env['KRB5CCNAME'] = '/host/tmp/krb5cc_%d' % pwd.getpwnam(self.user.name).pw_uid 

Grâce au code ci-dessus, l'utilisateur JupyterHub peut exécuter des commandes hdfs à partir du terminal Jupyter et exécuter des travaux Spark sans étapes d'authentification supplémentaires. Monter le répertoire / tmp entier de l'hôte dans le conteneur n'est pas sûr - nous sommes conscients de ce problème, mais sa solution est toujours en cours de développement.

Versions Python 2 et 3


Ici, il semblerait, tout est simple: vous devez installer les versions nécessaires de Python et les intégrer à Jupyter, créant le noyau nécessaire. Cette question a déjà été abordée dans de nombreux endroits. Conda est utilisé pour gérer les environnements Python. La raison pour laquelle toute simplicité n'est qu'apparente sera claire dans la section suivante. Exemple de noyau pour Python 3.6 (ce fichier n'est pas dans git - tous les fichiers du noyau sont générés par du code):

/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/share/jupyter/kernels/python3.6.6/kernel.json

 {   "argv": [      "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python",       "-m",       "ipykernel_launcher",       "-f",      "{connection_file}"   ],   "display_name": "Python 3",   "language": "python" } 

Spark 1 et 2


Pour s'intégrer aux clients SPARK, vous devez également créer des noyaux. Exemple de noyau pour Python 3.6 et SPARK 2.

/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/share/jupyter/kernels/python3.6.6-pyspark2/kernel.json

 {   "argv": [       "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python",       "-m",       "ipykernel_launcher",       "-f",      "{connection_file}"   ],   "display_name": "Python 3 + PySpark 2",   "language": "python",   "env": {       "JAVA_HOME": "/usr/java/default/",       "SPARK_HOME": "/opt/cloudera/parcels/SPARK2/lib/spark2/",       "PYTHONSTARTUP": "/opt/cloudera/parcels/SPARK2/lib/spark2/python/pyspark/shell.py",       "PYTHONPATH": "/opt/cloudera/parcels/SPARK2/lib/spark2/python/:/opt/cloudera/parcels/SPARK2/lib/spark2/python/lib/py4j-0.10.7-src.zip",       "PYSPARK_PYTHON": "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python"   } } 

Notez simplement que l'exigence de prise en charge de Spark 1 s'est développée historiquement. Cependant, il est possible que quelqu'un soit confronté à des restrictions similaires - vous ne pouvez pas, par exemple, installer Spark 2 dans un cluster. Par conséquent, nous décrivons ici les pièges que nous avons rencontrés sur le chemin de la mise en œuvre.
Premièrement, Spark 1.6.1 ne fonctionne pas avec Python 3.6. Fait intéressant, dans CDH 5.12.1 cela a été corrigé, mais dans 5.15.1 - pour une raison quelconque non). Au début, nous voulions résoudre ce problème en appliquant simplement le correctif approprié. Cependant, à l'avenir, cette idée a dû être abandonnée, car cette approche nécessite l'installation d'un Spark modifié dans un cluster, ce qui était inacceptable pour nous. La solution a été trouvée en créant un environnement Conda séparé avec Python 3.5.

Le deuxième problème empêche Spark 1 de fonctionner à l'intérieur de Docker. Le pilote Spark ouvre un port spécifique par lequel Worker se connecte au pilote - pour cela, le pilote lui envoie son adresse IP. Dans le cas de Docker Worker, il essaie de se connecter au pilote via l'IP du conteneur, et lorsque vous utilisez network = bridge, cela ne fonctionne pas tout à fait naturellement.

La solution évidente est d'envoyer non pas l'IP du conteneur, mais l'IP de l'hôte, qui a été implémentée dans Spark 2 en ajoutant les paramètres de configuration appropriés. Ce correctif a été repensé de façon créative et appliqué à Spark 1. Le Spark modifié de cette manière n'a pas besoin d'être placé sur les hôtes du cluster, il n'y a donc pas de problème similaire à l'incompatibilité avec Python 3.6.

Quelle que soit la version de Spark, pour sa fonctionnalité, il est nécessaire d'avoir les mêmes versions Python dans le cluster que dans le conteneur. Pour installer Anaconda en contournant directement Cloudera Manager, nous avons dû apprendre à faire deux choses:

  • construisez votre colis avec Anaconda et tous les bons environnements
  • installez-le dans Docker (par souci de cohérence)

Colis de montage Anaconda


Cela s'est avéré être une tâche assez simple. Tout ce dont vous avez besoin est:

  1. Préparez le contenu des colis en installant les versions requises de l'environnement Anaconda et Python
  2. Créez des fichiers de métadonnées et placez-les dans le répertoire des métadonnées
  3. Créer un colis avec du goudron simple
  4. Valider l'utilitaire de colis de Cloudera

Le processus est décrit plus en détail sur GitHub , il y a aussi un code de validation là-bas. Nous avons emprunté des métadonnées dans le colis officiel Anaconda pour Cloudera, en les retravaillant de manière créative.

Installer un colis dans Docker


Cette pratique s'est avérée utile pour deux raisons:

  • assurer l'opérabilité de Spark - il est impossible de placer Anaconda dans un cluster sans colis
  • Spark 2 est distribué uniquement sous forme de colis - vous pouvez bien sûr l'installer dans un conteneur juste sous forme de fichiers jar, mais cette approche a été rejetée

En prime, suite à la résolution des problèmes ci-dessus, nous avons reçu:

  • facilité de configuration des clients Hadoop et Spark - lors de l'installation des mêmes parcelles dans Docker et dans le cluster, les chemins d'accès sur le cluster et dans le conteneur sont les mêmes
  • facilité de maintien d'un environnement uniforme dans le conteneur et dans le cluster - lors de la mise à jour du cluster, l'image Docker est simplement reconstruite avec les mêmes parcelles que celles installées dans le cluster.

Pour installer le colis dans Docker, Cloudera Manager est d'abord installé à partir des packages RPM. Pour l'installation réelle du colis, le code Java est utilisé. Le client en Java sait ce que le client en Python ne peut pas faire, j'ai donc dû utiliser Java et perdre une certaine uniformité), qui appelle l'API.

assets / install-parcels / src / InstallParcels.java

 ParcelsResourceV5 parcels = clusters.getParcelsResource(clusterName); for (int i = 1; i < args.length; i += 2) {   result = installParcel(api, parcels, args[i], args[i + 1], pause);   if (!result) {       System.exit(1);   } } 

Limitation des ressources de l'hôte


Pour gérer les ressources de la machine hôte, une combinaison de DockerSpawner est utilisée - un composant qui exécute les utilisateurs finaux de Jupyter dans un conteneur Docker distinct - et des cgroups - un mécanisme de gestion des ressources sous Linux. DockerSpawner utilise l'API Docker, qui vous permet de définir le groupe de contrôle parent pour le conteneur. Il n'y a pas une telle possibilité dans le DockerSpawner normal, nous avons donc écrit un code simple qui nous permet de définir la correspondance entre les entités AD et le groupe de contrôle parent dans la configuration.

assets / jupyterhub / dsai.py

 def set_extra_host_config(self):       extra_host_config = {}       if self.user.name in self.user_cgroup_parent:           cgroup_parent = self.user_cgroup_parent[self.user.name]       else:           pw_name = pwd.getpwnam(self.user.name).pw_name           group_found = False           for g in grp.getgrall():               if pw_name in g.gr_mem and g.gr_name in self.group_cgroup_parent:                   cgroup_parent = self.group_cgroup_parent[g.gr_name]                   group_found = True                   break           if not group_found:               cgroup_parent = self.cgroup_parent extra_host_config['cgroup_parent'] = cgroup_parent 

Une petite modification a également été introduite qui lance Jupyter à partir de la même image à partir de laquelle JupyterHub est lancé. Par conséquent, il n'est pas nécessaire d'utiliser plusieurs images.

assets / jupyterhub / dsai.py

 current_container = None host_name = socket.gethostname() for container in self.client.containers():   if container['Id'][0:12] == host_name:       current_container = container       break self.image = current_container['Image'] 

Ce qui doit être exécuté exactement dans le conteneur, Jupyter ou JupyterHub, est déterminé dans le script de démarrage par des variables d'environnement:

assets / jupyterhub / dsai.py

 #!/bin/bash ANACONDA_PATH="/opt/cloudera/parcels/Anaconda/" DEFAULT_ENV=`cat ${ANACONDA_PATH}/envs/default` source activate ${DEFAULT_ENV} if [ -z "${JUPYTERHUB_CLIENT_ID}" ]; then   while true; do       jupyterhub -f /etc/jupyterhub/jupyterhub_config.py   done else   HOME=`su ${JUPYTERHUB_USER} -c 'echo ~'`   cd ~   su ${JUPYTERHUB_USER} -p -c "jupyterhub-singleuser --KernelSpecManager.ensure_native_kernel=False --ip=0.0.0.0" fi 

La possibilité de démarrer des conteneurs Docker Jupyter à partir du conteneur Docker JupyterHub est obtenue en montant le socket du démon Docker dans le conteneur JupyterHub.

start-hub.sh

 VOLUMES="-<b>v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z</b> -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

À l'avenir, il est prévu d'abandonner cette décision au profit, par exemple, de ssh.

Lors de l'utilisation de DockerSpawner en conjonction avec Spark, un autre problème se pose: le pilote Spark ouvre des ports aléatoires, par lesquels les travailleurs établissent ensuite une connexion externe. Nous pouvons contrôler la plage de numéros de port parmi lesquels des nombres aléatoires sont sélectionnés en définissant ces plages dans la configuration Spark. Cependant, ces plages doivent être différentes pour différents utilisateurs, car nous ne pouvons pas exécuter les conteneurs Jupyter avec les mêmes ports publiés. Pour résoudre ce problème, un code a été écrit qui génère simplement des plages de ports par ID utilisateur à partir de la base de données JupyterHub et lance le conteneur Docker et Spark avec la configuration appropriée:

assets / jupyterhub / dsai.py

 def set_extra_create_kwargs(self):       user_spark_driver_port, user_spark_blockmanager_port, user_spark_ui_port, user_spark_max_retries = self.get_spark_ports()       if user_spark_driver_port == 0 or user_spark_blockmanager_port == 0 or user_spark_ui_port == 0 or user_spark_max_retries == 0:           return       ports = {}       for p in range(user_spark_driver_port, user_spark_driver_port + user_spark_max_retries):           ports['%d/tcp' % p] = None       for p in range(user_spark_blockmanager_port, user_spark_blockmanager_port + user_spark_max_retries):           ports['%d/tcp' % p] = None       for p in range(user_spark_ui_port, user_spark_ui_port + user_spark_max_retries):           ports['%d/tcp' % p] = None self.extra_create_kwargs = { 'ports' : ports } 

L'inconvénient de cette solution est que lorsque vous redémarrez le conteneur avec JupyterHub, tout cesse de fonctionner en raison d'une perte de base de données. Par conséquent, lorsque vous redémarrez le JupyterHub pour, par exemple, un changement de configuration, nous ne touchons pas le conteneur lui-même, mais redémarrons uniquement le processus JupyterHub à l'intérieur.

restart-hub.sh

 #!/bin/bash docker ps | fgrep 'dsai1.2' | fgrep -v 'jupyter-' | awk '{ print $1; }' | while read ID; do docker exec $ID /bin/bash -c "kill \$( cat /root/jupyterhub.pid )"; done 

Les groupes de contrôle eux-mêmes sont créés par des outils Linux standard, la correspondance entre les entités AD et les groupes de contrôle dans la configuration ressemble à ceci.

 <b>config-example/jupyterhub/jupyterhub_config.py</b> c.DSAISpawner.user_cgroup_parent = {   'bank\\user1'    : '/jupyter-cgroup-1', # user 1   'bank\\user2'    : '/jupyter-cgroup-1', # user 2   'bank\\user3'    : '/jupyter-cgroup-2', # user 3 } c.DSAISpawner.cgroup_parent = '/jupyter-cgroup-3' 

Code Git


Notre solution est accessible au public sur GitHub: https://github.com/DS-AI/dsai/ (DSAI - Data Science and Artificial Intelligence). Tout le code est organisé dans des répertoires avec des numéros de série - le code de chaque répertoire suivant peut utiliser des artefacts du précédent. Le résultat du code du dernier répertoire sera une image Docker.

Chaque répertoire contient des fichiers:

  • assets.sh - création d'artefacts nécessaires à l'assemblage (téléchargement à partir d'Internet ou copie à partir des répertoires des étapes précédentes)
  • build.sh - construire
  • clean.sh - nettoyage des artefacts nécessaires pour l'assemblage

Afin de reconstruire complètement l'image Docker, il est nécessaire d'exécuter clean.sh, assets.sh, build.sh à partir des répertoires en fonction de leurs numéros de série.

Pour l'assemblage, nous utilisons une machine avec Linux RedHat 7.4, Docker 17.05.0-ce. La machine possède 8 cœurs, 32 Go de RAM et 250 Go d'espace disque. Il est fortement recommandé de ne pas utiliser d'hôte avec les pires paramètres de RAM et de disque dur pour le créer.

Voici l'aide pour les noms utilisés:

  • 01-spark-patched - RPM Spark 1.6.1 avec deux patchs SPARK-4563 et SPARK-19019 appliqués.
  • 02-validateur - validateur de colis
  • 03-anaconda-dsai-parcel-1.0 - parcelle Anaconda avec le bon Python (2, 3.5 et 3.6)
  • 04-cloudera-manager-api - Bibliothèques d'API Cloudera Manager
  • 05-dsai1.2-offline - image finale

Hélas, l'assemblage peut se bloquer pour des raisons que nous n'avons pas pu résoudre (par exemple, tar est abandonné pendant l'assemblage de la parcelle. Dans ce cas, en règle générale, il vous suffit de redémarrer l'assemblage, mais cela n'aide pas toujours (par exemple, l'assemblage Spark dépend de ressources externes Cloudera, qui peut ne plus être disponible, etc.).

Un autre inconvénient est que l'assemblage de colis est irréproductible. Comme les bibliothèques sont constamment mises à jour, la répétition de l'assembly peut donner un résultat différent du précédent.

Grande finale


Maintenant que les utilisateurs utilisent avec succès les outils, leur nombre a dépassé plusieurs dizaines et continue de croître. À l'avenir, nous prévoyons d'essayer JupyterLab et envisageons de connecter le GPU au cluster, car maintenant les ressources informatiques de deux serveurs d'applications assez puissants ne suffisent plus.

Source: https://habr.com/ru/post/fr443294/


All Articles