SchedulingSimulator/task/ai_task_submit.py

690 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import json
import time
import uuid
import requests
from typing import Dict, Optional, List
from threading import Lock
import logging # 导入 logging 模块
from config.config import TASK_STATUS as TASK_STATUS_COMMON
from config.db_manager import update_file_all_fields, update_cluster_resources
# 任务状态常量
TASK_STATUS = {
"PENDING": "pending",
"RUNNING": "running",
"SUCCEED": "Succeeded",
"FAILED": "failed",
"RETRY_EXHAUSTED": "retry_exhausted"
}
# 全局变量与锁
# 初始化日志记录器
logger = logging.getLogger(__name__)
dataset_map = {} # 存储数据集信息的映射
dataset_lock = Lock() # 数据集操作锁
task_map_lock = Lock() # 任务映射操作锁
cluster_lock = Lock() # 集群资源操作锁
cluster_resources = {} # 集群资源信息
API_CONFIG = {} # API配置
# 移除 YAML 配置文件路径,不再需要
# sonCode_cluster_mapping_path = "sonCode_cluster_mapping.yaml" # YAML配置文件路径
class DatasetInfo:
"""数据集信息类"""
def __init__(self, file_location: str, name: str, size: int, is_uploaded: bool,
upload_cluster: List[str], upload_time: str, dataset_target_id: str):
self.file_location = file_location
self.name = name
self.size = size
self.is_uploaded = is_uploaded
self.upload_cluster = upload_cluster
self.upload_time = upload_time
self.dataset_target_id = dataset_target_id
def __dict__(self):
return {
"file_location": self.file_location,
"name": self.name,
"size": self.size,
"is_uploaded": self.is_uploaded,
"upload_cluster": self.upload_cluster,
"upload_time": self.upload_time,
"dataset_target_id": self.dataset_target_id
}
def get_config_from_db() -> Dict:
"""从数据库获取配置"""
try:
from config.cache_manager import get_cached_config
return get_cached_config()
except Exception as e:
logger.error(f"从缓存加载配置失败: {str(e)}")
# 回退到直接从数据库加载
try:
from config.db_manager import load_config_from_db
return load_config_from_db()
except Exception as e:
logger.error(f"从数据库加载配置失败: {str(e)}")
return {}
UPLOAD_CLUSTER_MAPPING = {
"1865927992266463180": 1,
"1865927992266461184": 23,
"1790300942428540928": 3,
"1830873903296155648": 8,
"1830873578531228942": 9,
"1865927992266462181": 10,
"1830873903296155649": 11,
"1865927992266462182": 13,
"1865927992266462180": 14,
"1770703902472146944": 12
}
def upload_dataset(task: Dict, token) -> Optional[str]:
"""上传数据集并返回dataset_target_id"""
global task_name, copiedToFullRoots
try:
# 获取必要参数
cluster_id = task.get("cluster_id")
dataset_id = task.get("dataset_id")
card_type = task.get("card_type")
dataset_info = get_fileinfo_by_dataset_id(dataset_id)
is_uploaded = dataset_info.get("is_uploaded")
#如果文件已经上传完成并存在dataset_target_id则直接返回
dataset_target_id = dataset_info.get("dataset_target_id")
if dataset_target_id and is_uploaded:
logger.info(f"数据集已经上传数据集id为{dataset_target_id}")
return dataset_target_id
file_location = dataset_info.get("file_location")
file_name = dataset_info.get("dataset_name")
file_path = f"{file_location}{file_name}"
package_name = task.get("package_name")
if not package_name:
package_name = f"dataset_{uuid.uuid4().hex[:8]}" # 生成默认包名
task_name = task.get("task_name", "unknown_task")
# 检查文件是否存在
if not os.path.exists(file_path):
print(f"[{task_name}] 数据集文件不存在: {file_path}")
return None
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}
# 从数据库获取API配置
config_data = get_config_from_db()
api_config = config_data.get("api_config", {})
# 1. 创建数据集文件夹
create_config = api_config.get("create_package", {})
create_url = create_config.get("url", "http://jcc.jointcloud.net/jsm/jobSet/createPackage")
create_timeout = create_config.get("timeout", 10)
create_payload = {
"userID": 5,
"name": package_name,
"dataType": "dataset",
"packageID": 0,
"uploadPriority": {"type": "specify", "clusters": [cluster_id]},
"bindingInfo": {
"clusterIDs": [cluster_id],
"name": package_name,
"category": "image",
"type": "dataset",
"imageID": "",
"bias": [],
"region": [],
"chip": [card_type],
"selectedCluster": [],
"modelType": "",
"env": "",
"version": "",
"packageID": 0,
"points": 0
}
}
create_resp = requests.post(
create_url,
json=create_payload,
headers=headers,
timeout=create_timeout
)
create_resp.raise_for_status()
create_result = create_resp.json()
if create_result.get("code") != "OK":
raise ValueError(f"创建文件夹失败 | 响应: {create_result}")
packageID = create_result["data"]["newPackage"]["packageID"]
print(f"[{task_name}] 已创建数据集文件夹 | packageID: {packageID}")
# 2. 查询文件夹内容(验证创建结果)
query_url = "http://jcc.jointcloud.net/jsm/jobSet/queryUploaded"
query_payload = {
"queryParams": {
"dataType": "dataset",
"userID": 5,
"packageID": packageID,
"path": "",
"CurrentPage": 1,
"pageSize": 10,
"orderBy": "name"
}
}
query_resp = requests.post(query_url, json=query_payload, headers=headers, timeout=15)
query_resp.raise_for_status()
query_result = query_resp.json()
if query_result.get("code") != "OK":
raise ValueError(f"查询文件夹内容失败 | 响应: {query_result}")
print(f"[{task_name}] 文件夹内容查询成功")
# 3. 获取预签名URL用于上传文件到目标文件夹
#获取任务的调度类型
strategy = task.get("strategy")
# 根据调度策略选择集群
if strategy == "data":
info_data = {
"userID": 5,
"info": {
"type": "upload",
"params": {
"packageID": packageID,
"copyTo": [],
"copyToPath": [],
"path": file_name
}
}
}
else:
info_data = {
"userID": 5,
"info": {
"type": "upload",
"params": {
"packageID": packageID,
"copyTo": [UPLOAD_CLUSTER_MAPPING[cluster_id]],
"copyToPath": [f"/dataset/{package_name}/"],
"path": file_name
}
}
}
upload_url = "http://101.201.215.196:7891/storage/presign"
presign_resp = requests.post(
upload_url,
json=info_data,
headers={'Authorization': f'Bearer {token}'},
timeout=300
)
presign_resp.raise_for_status()
presign_result = presign_resp.json()
if presign_result.get("code") != "OK":
raise ValueError(f"获取预签名URL失败 | 响应: {presign_result}")
# 获取预签名URL
presign_url = presign_result["data"]["presignUrl"]
with open(file_path, "rb") as f:
file_data = f.read()
try:
upload_resp = requests.post(
url=presign_url,
data=file_data
)
# 打印响应结果
upload_resp.raise_for_status()
print(f"[{task_name}] 文件通过预签名URL上传成功")
upload_result = upload_resp.json()
if upload_result.get("code") != "OK":
raise ValueError(f"文件上传失败 | 响应: {upload_result}")
object_id = upload_result["data"]["object"]["objectID"]
copiedToFullRoots = upload_result["data"]["copiedToFullRoots"]
print(f"[{task_name}] 文件上传成功 | objectID: {object_id}")
except Exception as e:
print(f"上传失败,错误信息:{str(e)}")
# 4. 通知上传完成
notify_config = api_config.get("notify_upload", {})
notify_url = notify_config.get("url", "http://jcc.jointcloud.net/jsm/jobSet/notifyUploaded")
notify_timeout = notify_config.get("timeout", 15)
notify_payload = {
"userID": 5,
"packageID": packageID,
"uploadParams": {
"copiedTo": [UPLOAD_CLUSTER_MAPPING[cluster_id]],
"copiedToFullRoots": copiedToFullRoots,
"dataType": "dataset",
"uploadInfo": {
"type": "local",
"localPath": file_name,
"objectIDs": [object_id] # 补充objectID
}
}
}
notify_resp = requests.post(
notify_url,
json=notify_payload,
headers=headers,
timeout=notify_timeout
)
notify_resp.raise_for_status()
notify_result = notify_resp.json()
if notify_result.get("code") != "OK":
raise ValueError(f"通知上传完成失败 | 响应: {notify_result}")
print(f"[{task_name}] 已通知上传完成")
# 5. 二次查询上传内容
query_resp2 = requests.post(query_url, json=query_payload, headers=headers, timeout=15)
query_resp2.raise_for_status()
query_result2 = query_resp2.json()
if query_result2.get("code") != "OK":
raise ValueError(f"二次查询失败 | 响应: {query_result2}")
print(f"[{task_name}] 二次查询上传内容成功")
# 6. 查询上传状态
status_url = "http://jcc.jointcloud.net/jsm/jobSet/uploadStatus"
status_payload = {
"userID": 5,
"operate": "query",
"packageID": packageID,
"dataType": "code" # 修正为dataset类型
}
status_resp = requests.post(status_url, json=status_payload, headers=headers, timeout=15)
status_resp.raise_for_status()
status_result = status_resp.json()
# if status_result.get("code") != "OK":
# raise ValueError(f"查询上传状态失败 | 响应: {status_result}")
if status_resp.status_code == 200:
upload_status_result = status_resp.json()
print(f"查询上传状态: {upload_status_result}")
else:
print(f"查询上传状态,状态码: {status_resp.status_code}")
return
# print(f"[{task_name}] 上传状态: {status_result['data'].get('status')}")
# 7. 绑定数据集到集群
bind_config = api_config.get("bind_cluster", {})
bind_url = bind_config.get("url", "http://jcc.jointcloud.net/jsm/jobSet/binding")
bind_timeout = bind_config.get("timeout", 300)
bind_payload = {
"userID": 5,
"info": {"type": "dataset", "packageID": packageID, "clusterIDs": [cluster_id]}
}
bind_resp = requests.post(
bind_url,
json=bind_payload,
headers=headers,
timeout=300
)
bind_resp.raise_for_status()
bind_result = bind_resp.json()
if bind_result.get("code") != "OK":
raise ValueError(f"绑定集群失败 | 响应: {bind_result}")
print(f"[{task_name}] 已绑定集群 {cluster_id}")
# 8. 获取dataset_target_id
query_binding_url = "http://jcc.jointcloud.net/jsm/jobSet/queryBinding"
query_binding_payload = {
"dataType": "dataset",
"param": {"userID": 5, "bindingID": -1, "type": "private"}
}
binding_resp = requests.post(query_binding_url, json=query_binding_payload, headers=headers, timeout=15)
binding_resp.raise_for_status()
binding_result = binding_resp.json()
if binding_result.get("code") != "OK":
raise ValueError(f"查询绑定信息失败 | 响应: {binding_result}")
# 提取目标ID
dataset_target_id = None
for data in binding_result["data"]["datas"]:
if data["info"]["name"] == package_name:
dataset_target_id = data["ID"]
break
if not dataset_target_id:
raise ValueError(f"未找到 {package_name} 对应的绑定ID")
print(f"[{task_name}] 成功获取dataset_target_id: {dataset_target_id}")
# 保存数据集信息
with dataset_lock:
dataset_map[file_location] = DatasetInfo(
file_location=file_location,
name=file_name,
size=os.path.getsize(file_path),
is_uploaded=True,
upload_cluster=[cluster_id],
upload_time=time.strftime("%Y-%m-%d %H:%M:%S"),
dataset_target_id=dataset_target_id
)
#更新数据库中file_mapping表的数据
if update_file_all_fields(dataset_id, dataset_target_id=dataset_target_id, is_uploaded=1):
logger.info(
f"成功更新文件 {dataset_id} 在数据库中的dataset_target_id为 {dataset_target_id}")
return dataset_target_id
except Exception as e:
print(f"[{task_name}] 数据集上传失败: {str(e)}")
return None
def get_file_info(cluster_id, file_location, file_name,card_type):
""""#根据code_id和cluster_id获取son_code_id"""
try:
# 从数据库获取算法映射配置
config_data = get_config_from_db()
file_mapping = config_data.get('file_mapping', {})
for file_id, info in file_mapping.items():
if cluster_id == str(info["cluster_id"]) and file_location == info["file_location"] and file_name == info["dataset_name"] and card_type == info["card_type"]:
return info
except Exception as e:
logger.error(f"从数据库查询 文件信息 失败: {str(e)}")
return None
def get_fileinfo_by_dataset_id(dataset_id):
"""根据 dataset_id 从数据库中查询 code_id"""
try:
# 从数据库获取算法映射配置
config_data = get_config_from_db()
file_mapping = config_data.get('file_mapping', {})
for file_id, info in file_mapping.items():
if dataset_id == file_id:
return info
except Exception as e:
logger.error(f"从数据库查询 文件信息 失败: {str(e)}")
return None
def query_resource_info(card_type, cluster_id, token):
"""调用接口查询资源信息"""
try:
# 接口URL
API_URL = "https://jcc.jointcloud.net/jsm/jobSet/queryResource"
# 请求参数
payload = {
"queryResource": {
"cpu": {"min": 0, "max": 0},
"memory": {"min": 0, "max": 0},
"gpu": {"min": 0, "max": 0},
"storage": {"min": 0, "max": 0},
"type": card_type
},
"calcNetworkCost": False,
"resourceType": "Train",
"clusterIDs": [cluster_id]
}
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}
# 发送请求并设置超时
response = requests.post(
url=API_URL,
json=payload,
headers=headers,
timeout=10 # 10秒超时设置
)
response.raise_for_status() # 触发HTTP错误状态码的异常
# 解析响应
result = response.json()
# 验证响应状态
if result.get("code") != "OK":
logger.error(f"接口返回非成功状态: {result.get('message', '未知错误')}")
return None
# 提取资源列表
resources = result.get("data", {}).get("resource", [])
if not resources:
logger.info("未获取到任何资源数据")
return None
# 筛选符合条件的资源
for resource in resources:
# 严格验证类型和集群ID
if resource.get("type") == card_type and resource.get("clusterId") in cluster_id:
# 特殊处理 章鱼部分规格不可用
if card_type == "ILUVATAR-GPGPU" and cluster_id == "1865927992266462182":
return "BI-V100"
resource_name = resource.get("name")
if resource_name:
logger.info(f"找到符合条件的资源: {resource_name}")
return resource_name
logger.warning("找到符合条件的资源,但名称为空")
# 未找到符合条件的资源
logger.info(f"没有找到类型为{card_type}且属于集群{cluster_id}的资源")
return None
except requests.exceptions.Timeout:
logger.error("请求接口超时")
return None
except requests.exceptions.RequestException as e:
logger.error(f"请求接口失败: {str(e)}")
return None
except ValueError as e:
logger.error(f"解析响应JSON失败: {str(e)}")
return None
# 提交单个AI任务到集群
def submit_ai_task(task: Dict, token) -> Optional[str]:
# 加载API配置
global API_CONFIG, TASK_STATUS
config_data = get_config_from_db()
API_CONFIG = config_data.get('api_config', {})
# 获取选择的集群ID
cluster_id = task.get("cluster_id")
resource_name = query_resource_info(task.get("card_type"), cluster_id, token)
if not cluster_id:
with task_map_lock:
task["status"] = TASK_STATUS_COMMON["FAILED"]
task["error_msg"] = "未指定集群ID"
print(f"[{task['task_name']}] 提交失败未指定集群ID")
return None
# 数据集处理获取dataset_target_id
dataset_target_id = upload_dataset(task, token)
if task.get("type") == "Ai" and task.get("dataset_name") and not dataset_target_id:
# 构造资源变化字典
task_resource = task.get("resource", {})
resource_changes = {}
# 计算资源变化量(负值表示扣减)
for res_type in ["CPU", "MEMORY"]:
required = task_resource.get(res_type, 0)
if required > 0:
resource_changes[res_type] = +required
# 处理加速器资源
for acc_type in [key for key in task_resource if key not in ["CPU", "MEMORY"]]:
required_count = task_resource.get(acc_type, 0)
if required_count > 0:
resource_changes[acc_type] = +required_count
# 使用批量操作更新数据库
if update_cluster_resources(cluster_id, resource_changes):
logger.info(f"成功从数据库恢复集群 {cluster_id} 的资源: {resource_changes}")
else:
logger.error(f"从数据库恢复集群 {cluster_id} 的资源失败: {resource_changes}")
print(f"[{task['task_name']}] 提交失败:数据集处理失败")
return None
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}
task_name = task["task_name"]
## code_id = task["code_id"]
card_type = task["card_type"]
# 从数据库中查询 son_code_id
fileInfo = get_fileinfo_by_dataset_id(task["dataset_id"])
file_location = fileInfo.get("file_location")
file_name = fileInfo.get("dataset_name")
file_info = get_file_info(cluster_id,file_location,file_name,card_type)
son_code_id = file_info.get("son_code_id")
image_id = file_info.get("image_id")
# 检查son_code_id是否有效
if not son_code_id:
error_msg = "无法获取有效的son_code_id任务提交失败"
print(f"[{task_name}] {error_msg}")
# 构造资源变化字典
task_resource = task.get("resource", {})
resource_changes = {}
# 计算资源变化量(负值表示扣减)
for res_type in ["CPU", "MEMORY"]:
required = task_resource.get(res_type, 0)
if required > 0:
resource_changes[res_type] = +required
# 处理加速器资源
for acc_type in [key for key in task_resource if key not in ["CPU", "MEMORY"]]:
required_count = task_resource.get(acc_type, 0)
if required_count > 0:
resource_changes[acc_type] = +required_count
# 使用批量操作更新数据库
if update_cluster_resources(cluster_id, resource_changes):
logger.info(f"成功从数据库恢复集群 {cluster_id} 的资源: {resource_changes}")
else:
logger.error(f"从数据库恢复集群 {cluster_id} 的资源失败: {resource_changes}")
return None
try:
# 1. 提交训练任务
submit_config = API_CONFIG.get("submit_task", {})
config = submit_config.get("url", "http://jcc.jointcloud.net/jsm/jobSet/submit")
submit_timeout = submit_config.get("timeout", 100)
task_res = task["resource"]
# 获取加速器类型如NPU
acc_type = next((key for key in task_res if key not in ["CPU", "MEMORY"]), None)
submit_payload = {
"userID": 5,
"jobSetInfo": {
"jobs": [
{
"localJobID": "1",
"name": task_name,
"description": "",
"type": "AI",
"files": {
"dataset": {"type": "Binding", "bindingID": dataset_target_id},
"model": {"type": "Binding", "bindingID": None},
"image": {"type": "Image", "imageID": image_id}
},
"jobResources": {
"scheduleStrategy": "dataLocality",
"clusters": [
{
"code": {"type": "Binding", "bindingID": son_code_id},
"clusterID": cluster_id,
"runtime": {"envs": {}, "params": {}},
"resources": [
{"type": "CPU", "name": "CPU", "number": task_res["CPU"]},
{"type": "MEMORY", "name": "RAM", "number": task_res["MEMORY"]},
{"type": acc_type, "name": resource_name, "number": task_res.get(acc_type, 0)}
]
}
]
}
}
]
}
}
response = requests.post(
config,
json=submit_payload,
headers=headers,
timeout=submit_timeout
)
response.raise_for_status()
submit_resp = response.json()
if submit_resp.get("code") != "OK":
raise ValueError(f"任务提交失败 | API返回: {submit_resp}")
third_party_task_id = submit_resp.get('data', {}).get('jobSetID')
print(f"[{task_name}] 任务提交至集群 {cluster_id} 成功 | 云际任务ID: {third_party_task_id}")
# 更新任务状态为已提交到数据库
from config.db_manager import update_task_status
from config.config import TASK_STATUS
if not update_task_status(task_name, TASK_STATUS["SUBMITTING"], third_party_task_id):
logger.error(f"更新任务 {task_name} 状态到数据库失败")
return third_party_task_id
except Exception as e:
error_msg = f"提交失败: {str(e)}"
# 构造资源变化字典
task_resource = task.get("resource", {})
resource_changes = {}
# 计算资源变化量(负值表示扣减)
for res_type in ["CPU", "MEMORY"]:
required = task_resource.get(res_type, 0)
if required > 0:
resource_changes[res_type] = +required
# 处理加速器资源
for acc_type in [key for key in task_resource if key not in ["CPU", "MEMORY"]]:
required_count = task_resource.get(acc_type, 0)
if required_count > 0:
resource_changes[acc_type] = +required_count
# 使用批量操作更新数据库
if update_cluster_resources(cluster_id, resource_changes):
logger.info(f"成功从数据库恢复集群 {cluster_id} 的资源: {resource_changes}")
else:
logger.error(f"从数据库恢复集群 {cluster_id} 的资源失败: {resource_changes}")
task["fail_count"] = task.get("fail_count", 0) + 1
max_fail = task.get("max_fail_threshold", 3)
if task["fail_count"] >= max_fail:
task["status"] = TASK_STATUS["RETRY_EXHAUSTED"]
else:
task["status"] = TASK_STATUS["FAILED"]
task["error_msg"] = error_msg
print(f"[{task_name}] {error_msg}")
return None