یک راه حل ساده برای مدیریت یادگیری ML مبتنی بر ابر – قسمت 2
این ادامه پست اخیر در موضوع ساخت راه حل های سفارشی مبتنی بر ابر برای توسعه مدل یادگیری ماشین (ML) با استفاده از خدمات ارائه نمونه سطح پایین است. تمرکز ما در این پست بر روی Amazon EC2 خواهد بود.
ارائه دهندگان خدمات ابری (CSP) معمولاً راه حل های کاملاً مدیریت شده برای آموزش مدل های ML در فضای ابری ارائه می دهند. به عنوان مثال، Amazon SageMaker، سرویس توسعه مدیریت ML آمازون، فرآیند یادگیری را بسیار ساده می کند. SageMaker نه تنها اجرای پایان به انتها آموزش را خودکار میکند – از تهیه خودکار انواع نمونههای درخواستی، راهاندازی محیط آموزشی، اجرای بار تمرینی، ذخیره مصنوعات آموزشی، و خاموش کردن همه آنها – بلکه یک تعدادی از خدمات جانبی که از توسعه ML پشتیبانی می کنند، مانند تنظیم خودکار مدل، کتابخانه های آموزشی توزیع شده بهینه شده برای پلت فرم، و موارد دیگر. با این حال، همانطور که اغلب در مورد راه حل های سطح بالا اتفاق می افتد، افزایش سهولت استفاده از آموزش SageMaker با سطحی از دست دادن کنترل بر جریان اصلی همراه است.
در پست قبلی، به برخی از محدودیتهایی که گاهی اوقات توسط سرویسهای یادگیری مدیریتشده مانند SageMaker اعمال میشود، اشاره کردیم، از جمله کاهش امتیازات کاربر، در دسترس نبودن برخی از انواع نمونهها، کاهش کنترل بر روی استقرار چند گره، و موارد دیگر. برخی از سناریوها به سطح بالاتری از استقلال در رابطه با مشخصات محیط یادگیری و جریان نیاز دارند. در این پست، یک رویکرد برای رسیدگی به این موارد را با ایجاد یک راه حل آموزشی سفارشی ساخته شده در بالای Amazon EC2 نشان می دهیم.
با تشکر فراوان از مکس رابین برای مشارکتش در این پست.
در پست قبلیمان، حداقل مجموعهای از ویژگیهایی را که از یک راهحل یادگیری خودکار نیاز داریم فهرست کردهایم و در ادامه یک راه برای پیادهسازی آنها در Google Cloud Platform (GCP) را به صورت گام به گام نشان دادیم. و در حالی که توالی مراحل مشابه برای هر پلتفرم ابری دیگری اعمال می شود، جزئیات به دلیل تفاوت های ظریف منحصر به فرد هر یک می تواند کاملاً متفاوت باشد. هدف ما در این پست ارائه یک پیاده سازی مبتنی بر EC2 آمازون با استفاده از دستور create_instance از AWS Python SDK (نسخه 1.34.23) خواهد بود. مانند پست قبلی، ما با یک دستور ساده برای ایجاد یک نمونه EC2 شروع می کنیم و به تدریج آن را با اجزای اضافی که شامل ویژگی های مدیریتی مورد نظر ما می شود، تقویت می کنیم. دستور create_instances از کنترل های زیادی پشتیبانی می کند. برای هدف تظاهرات خود، ما فقط بر مواردی تمرکز خواهیم کرد که با راه حل ما مرتبط هستند. ما وجود یک VPC پیشفرض و یک نمایه نمونه IAM را با مجوزهای مناسب (از جمله دسترسی به سرویسهای Amazon EC2، S3 و CloudWatch) فرض میکنیم.
توجه داشته باشید که راه های متعددی برای استفاده از Amazon EC2 برای انجام حداقل مجموعه ای از عملکردهایی که تعریف کردیم وجود دارد. ما انتخاب کردیم که یک پیاده سازی ممکن را نشان دهیم. لطفاً انتخاب ما از AWS، EC2 یا هر جزئیات پیادهسازی خاصی را که به عنوان تأیید انتخاب کردهایم تفسیر نکنید. بهترین راه حل آموزش ML تا حد زیادی به نیازها و جزئیات پروژه شما بستگی دارد.
ما با یک مثال حداقلی از یک درخواست واحد برای یک نمونه EC2 شروع می کنیم. ما یک نوع نمونه g5.xlarge با شتاب GPU و یک AMI آموزش عمیق اخیر (با سیستم عامل اوبونتو 20.4) را انتخاب کردیم.
import boto3region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type="g5.xlarge" # replace with instance of choice
ec2 = boto3.resource('ec2', region_name=region)
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
)
اولین پیشرفتی که میخواهیم پیادهسازی کنیم این است که بهمحض راهاندازی نمونهمان، حجم کار آموزشی بهطور خودکار اجرا شود، بدون نیاز به مداخله دستی. برای این منظور استفاده خواهیم کرد داده های کاربر آرگومان create_instances API که به شما امکان می دهد مشخص کنید چه چیزی در هنگام راه اندازی اجرا شود. در بلوک کد زیر، دنباله ای از دستورات را ارائه می دهیم که محیط آموزشی را تنظیم می کند (یعنی به روز رسانی مسیر متغیر محیطی برای اشاره به محیط از پیش ساخته شده PyTorch موجود در تصویر ما)، کد آموزشی ما را از آمازون S3 دانلود میکند، وابستگیهای پروژه را نصب میکند، اسکریپت آموزشی را اجرا میکند، و مصنوعات منبع را با یک مخزن دائمی S3 همگامسازی میکند. دمو فرض میکند که کد آموزشی قبلاً ایجاد شده و در فضای ابری آپلود شده است و شامل دو فایل است: یک فایل نیازمندی (الزامات. txt) و یک اسکریپت آموزشی مستقل (train.py). در عمل، محتوای دقیق دنباله راه اندازی به پروژه بستگی دارد. ما یک اشاره گر به نمایه نمونه از پیش تعریف شده IAM خود اضافه می کنیم که برای دسترسی به S3 لازم است.
import boto3region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type="g5.xlarge" # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/artifacts
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script
)
توجه داشته باشید که اسکریپت بالا، مصنوعات آموزشی را فقط در پایان آموزش همگام سازی می کند. یک راه حل مقاوم تر در برابر خطا، نقاط کنترل میانی مدل را در طول آموزش همگام می کند.
هنگامی که با استفاده از یک سرویس مدیریت شده تمرین می کنید، به محض تکمیل اسکریپت، نمونه های شما به طور خودکار خاموش می شوند تا اطمینان حاصل شود که فقط برای آنچه نیاز دارید پرداخت می کنید. در بلوک کد زیر، یک دستور self-destruct را در انتهای خود اضافه می کنیم داده های کاربر سناریو. ما این کار را با استفاده از دستور AWS CLI terminate-instances انجام می دهیم. دستور از ما می خواهد که بدانیم instance-id و میزبانی منطقه نمونه ما، که از ابرداده نمونه بازیابی می کنیم. اسکریپت به روز شده ما فرض می کند که نمایه نمونه IAM ما مجوز مناسب برای پایان دادن به یک نمونه را دارد.
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH# download and unpack code
aws s3 cp s3://my-s3-path/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
ما قویاً توصیه میکنیم که مکانیسمهای اضافی برای تأیید حذف یک نمونه مناسب اجرا شود تا از احتمال انباشته شدن هزینههای غیرضروری در موارد استفاده نشده («یتیم») در سیستم جلوگیری شود. در پست اخیر، ما نشان دادیم که چگونه می توان از ویژگی های بدون سرور برای رفع این نوع مشکل استفاده کرد.
آمازون EC2 به شما امکان می دهد با استفاده از تگ های نمونه EC2، ابرداده های سفارشی را به نمونه خود اعمال کنید. این به شما امکان می دهد اطلاعات مربوط به بار آموزشی و/یا محیط آموزشی را به نمونه ارسال کنید. در اینجا ما استفاده می کنیم مشخصات برچسب تنظیم برای ارسال یک نام نمونه و یک شناسه شغل آموزشی منحصر به فرد. ما از شناسه منحصر به فرد برای تعریف یک مسیر S3 ویژه برای مصنوعات کار خود استفاده می کنیم. توجه داشته باشید که ما باید صراحتاً نمونه را فعال کنیم تا از طریق آن به تگ های ابرداده دسترسی داشته باشد گزینه های فراداده تنظیمات.
import boto3region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type="g5.xlarge" # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'}
]
}],
)
استفاده از تگ های فراداده برای انتقال اطلاعات به نمونه های ما به ویژه در بخش های بعدی مفید خواهد بود.
به طور طبیعی، ما به توانایی تجزیه و تحلیل گزارش های خروجی برنامه خود در حین و بعد از آموزش نیاز داریم. این امر مستلزم آن است که آنها به صورت دوره ای با ثبت مداوم همگام شوند. در این پست، ما این را با استفاده از آمازون CloudWatch پیاده سازی می کنیم. در زیر یک فایل پیکربندی حداقل JSON برای فعال کردن کلکسیون گزارش CloudWatch تعریف میکنیم که به کد منبع tar-ball خود اضافه میکنیم. cw_config.json. لطفاً برای جزئیات در مورد راه اندازی و پیکربندی CloudWatch به اسناد رسمی مراجعه کنید.
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/output.log",
"log_group_name": "/aws/ec2/training-jobs",
"log_stream_name": "job-id"
}
]
}
}
}
}
در واقع ما دوست داریم log_stream_name برای شناسایی منحصر به فرد کار آموزشی. برای انجام این کار، از دستور sed برای جایگزینی رشته “job-id” عمومی با تگ metadata job-id از بخش قبل استفاده می کنیم. اسکریپت بهبود یافته ما همچنین شامل دستور شروع CloudWatch و تغییراتی برای هدایت خروجی استاندارد به خروجی های تعریف شده است. خروجی.log در فایل پیکربندی CloudWatch تعریف شده است.
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# configure cloudwatch
sed -i "s/job-id/${JOB_ID}/g" cw_config.json
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -c file:cw_config.json -s
# install dependencies
python3 -m pip install -r requirements.txt 2>&1 | tee -a output.log
# run training workload
python3 train.py 2>&1 | tee -a output.log
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
امروزه، اجرای کارهای آموزشی بر روی چندین گره به صورت موازی بسیار رایج است. تغییر کد درخواست نمونه ما برای پشتیبانی از چندین گره، یک موضوع ساده برای اصلاح آن است تعداد_نمونه ها تنظیمات. چالش این است که چگونه محیط را به گونه ای پیکربندی کنیم که از یادگیری توزیع شده پشتیبانی کند. راهی که انتقال داده ها را بین نمونه ها امکان پذیر و بهینه می کند.
برای به حداقل رساندن تأخیر شبکه بین نمونه ها و به حداکثر رساندن توان عملیاتی، ما یک اشاره گر به یک گروه قرارگیری خوشه از پیش تعریف شده اضافه می کنیم. تعیین سطح زمینه درخواست نمونه ec2 ما. خط فرمان زیر ایجاد یک گروه قرار دادن خوشه را نشان می دهد.
aws ec2 create-placement-group --group-name cluster-placement-group \
--strategy cluster
برای اینکه مصادیق ما با یکدیگر ارتباط برقرار کنند باید از حضور یکدیگر آگاه باشند. در این پست، حداقل پیکربندی محیط مورد نیاز برای اجرای آموزش داده های موازی در PyTorch را نشان خواهیم داد. برای PyTorch DistributedDataParallel (DDP)، هر نمونه باید IP گره اصلی، پورت اصلی، تعداد کل نمونه ها و شماره سریال آن را بداند. رتبه در میان تمام گره ها اسکریپت زیر پیکربندی یک کار آموزش داده موازی را با استفاده از متغیرهای محیطی نشان می دهد MASTER_ADDR، MASTER_PORT، NUM_NODESو NODE_RANK.
import os, ast, socket
import torch
import torch.distributed as dist
import torch.multiprocessing as mpdef mp_fn(local_rank, *args):
# discover topology settings
num_nodes = int(os.environ.get('NUM_NODES',1))
node_rank = int(os.environ.get('NODE_RANK',0))
gpus_per_node = torch.cuda.device_count()
world_size = num_nodes * gpus_per_node
node_rank = nodes.index(socket.gethostname())
global_rank = (node_rank * gpus_per_node) + local_rank
print(f'local rank {local_rank} '
f'global rank {global_rank} '
f'world size {world_size}')
# init_process_group assumes the existence of MASTER_ADDR
# and MASTER_PORT environment variables
dist.init_process_group(backend='nccl',
rank=global_rank,
world_size=world_size)
torch.cuda.set_device(local_rank)
# Add training logic
if __name__ == '__main__':
mp.spawn(mp_fn,
args=(),
nprocs=torch.cuda.device_count())
رتبه گره را می توان از ami-launch-index بازیابی کرد. تعداد گره ها و پورت اصلی در طول فراخوانی با create_instances شناخته می شوند و می توانند به عنوان تگ های نمونه EC2 ارسال شوند. با این حال، آدرس IP گره اصلی تنها پس از ایجاد نمونه اصلی تعیین می شود و تنها پس از فراخوانی create_instances می توان به نمونه ها ارتباط داد. در بلوک کد زیر، ما انتخاب کردیم که آدرس اصلی را با استفاده از یک فراخوانی خاص به API AWS Python SDK create_tags به هر یک از نمونهها ارسال کنیم. ما از همان فراخوانی برای به روز رسانی تگ نام هر نمونه با توجه به مقدار شاخص بوت استرپ آن استفاده می کنیم.
راه حل کامل آموزش چند گره در زیر ظاهر می شود:
import boto3region = 'us-east-1'
job_id = 'my-multinode-experiment' # replace with unique id
num_instances = 4
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type="g5.xlarge" # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
placement_group = 'cluster-placement-group' # replace with placement group
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export NODE_RANK=$(curl $CURL_FLAGS $INST_MD/ami-launch-index)
export NUM_NODES=$(curl $CURL_FLAGS $INST_MD/NUM_NODES)
export MASTER_PORT=$(curl $CURL_FLAGS $INST_MD/tags/instance/MASTER_PORT)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# configure cloudwatch
sed -i "s/job-id/${JOB_ID}_${NODE_RANK}/g" cw_config.json
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -c file:cw_config.json -s
# install dependencies
python3 -m pip install -r requirements.txt 2>&1 | tee -a output.log
# retrieve master address
# should be available but just in case tag application is delayed...
while true; do
export MASTER_ADDR=$(curl $CURL_FLAGS $INST_MD/tags/instance/MASTER_ADDR)
if [[ $MASTER_ADDR == "<?xml"* ]]; then
echo 'tags missing, sleep for 5 seconds' 2>&1 | tee -a output.log
sleep 5
else
break
fi
done
# run training workload
python3 train.py 2>&1 | tee -a output.log
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'},
{'Key': 'MASTER_PORT', 'Value': '7777'},
{'Key': 'NUM_NODES', 'Value': f'{num_instances}'}
]
}],
Placement={'GroupName': placement_group}
)
if num_instances > 1:
# find master_addr
for inst in instances:
if inst.ami_launch_index == 0:
master_addr = inst.network_interfaces_attribute[0]['PrivateIpAddress']
break
# update ec2 tags
for inst in instances:
res = ec2.create_tags(
Resources=[inst.id],
Tags=[
{'Key': 'NAME', 'Value': f'test-vm-{inst.ami_launch_index}'},
{'Key': 'MASTER_ADDR', 'Value': f'{master_addr}'}]
)
یک راه محبوب برای کاهش هزینه های آموزشی استفاده از نمونه های تخفیف دار آمازون EC2 Spot است. استفاده مؤثر از نمونههای Spot مستلزم آن است که راهی برای تشخیص قطعیها (مثلاً با گوش دادن به اعلانهای پایان کار) و اقدامات مناسب (مانند از سرگیری بارهای کاری ناقص) اجرا کنید. در زیر نشان می دهیم که چگونه اسکریپت خود را برای استفاده از نمونه های Spot تغییر دهیم InstanceMarketOptions راه اندازی API.
import boto3region = 'us-east-1'
job_id = 'my-spot-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type="g5.xlarge" # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
placement_group = 'cluster-placement-group' # replace with placement group
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'},
]
}],
InstanceMarketOptions = {
'MarketType': 'spot',
'SpotOptions': {
"SpotInstanceType": "one-time",
"InstanceInterruptionBehavior": "terminate"
}
}
)
لطفاً پستهای قبلی ما (مثلاً اینجا و اینجا) را برای ایدههایی در مورد نحوه پیادهسازی یک راهحل مدیریت چرخه حیات نمونه Spot ببینید.
خدمات ابری مدیریت شده برای توسعه هوش مصنوعی می تواند آموزش مدل را ساده کرده و نوار ورود را برای مدیران بالقوه پایین بیاورد. با این حال، شرایطی وجود دارد که کنترل بیشتری بر فرآیند یادگیری مورد نیاز است. در این پست، ما یک رویکرد را برای ایجاد یک محیط یادگیری مدیریت شده سفارشی در بالای Amazon EC2 نشان دادیم. البته جزئیات دقیق راه حل تا حد زیادی به نیازهای خاص هر پروژه بستگی دارد.
مثل همیشه لطفاً با نظرات، سؤالات یا اصلاحات به این پست پاسخ دهید.