前端使用 Python 的优缺点
优点:
- 很多逻辑腰放到后端实现,导致后端逻辑太复杂。比如 3 天闭源的逻辑,如果放在后端,后端需要获取数据后,先计算出需要展示的,然后再获得 submit 信息,然后返回前端。前端根据结果去判断是否要展示。这个判断是否 3 天内的逻辑不能放在前端,因为平台的访问者不能直接通过 get 请求获得 submit 的 config 内容,如果更改权限允许的话,那么其他 submit config 就会被泄露了。如果是后端查看当前是普通用户,再判断 3 天的闭源 or 自己是作者的情况,来判断是否返回 submit config 好像有点耦合了,权限问题与获取耦合到一起了。
缺点:
- 前端需要 Python 和 HTML、CSS、JS 都要会
前端响应缓慢
最初的逻辑
- 先获取当前 ongoing 和 finished 状态的 benchmark 信息
- 之后,再针对每个 benchmark 获取当前 benchmark 的 activity 状态(submit-num、contributor-num)
存在的问题
- benchmark 增加后,每一次都要请求后端 N 次,会导致后端的服务压力很大
更改后的逻辑
- 对请求进行合并。增加指定多个 name 后,获取多个 benchmark 的 activity 状态;若没有提供,则提供全量的状态
- 前端增加缓存。由于当前 benchmark 的活跃状态并不是一个需要实时更新的数据,因此允许短时的旧数据。应用程序内的缓存方式是,value 存储结果以及对应的存储时间,是一个 tuple。
更改后的效果
- 前端响应速度提升,从 3-5s 等待时间降低至不到 1s。
当然,现在又变慢了,原因是榜单数据增加后,python GIL 处理请求慢,需要渲染 jinjia。
@app.route("/")
@app.route("/competitions")
def competitions_handling():
benchmarks = json.loads(requests.get("%s/benchmark/filter?version=latest&status=ONGOING,FINISHED"%LEADERBOARD_BE.root_url).text)
if benchmarks["success"]: benchmarks = benchmarks["data"]
else: benchmarks = []
for benchmark in benchmarks:
activity_info = json.loads(requests.get("%s/benchmark/activity?name=%s"%(LEADERBOARD_BE.root_url, benchmark["name"])).text)
benchmark["submit_num"] = activity_info["data"].get("submit_num", 0)
benchmark["contributor_num"] = activity_info["data"].get("contributor_num", 0)
benchmark["leaderboard_url"] = "/competition_detail?name=%s&version=%s"%(benchmark["name"], benchmark["version"])
return render_template("competitions.jinja", auth_url=LEADERBOARD_BE.root_url, competitions=benchmarks)
@app.route("/competition_detail")
@async_update_cache(
timeout=10,
make_key_func=[cache_GET_params("name", "version"), cache_roles("initializing_benchmark_viewer")])
def competition_detail_handling():
pass
caches = dict()
def async_update_cache(timeout, make_key_func):
global caches
sentinel = object()
ttl = timedelta(seconds=timeout)
def async_update_task(ctx, key, f, args, kwargs):
with ctx:
value = ctx.app.ensure_sync(f)(*args, **kwargs)
assert f.__qualname__ in caches
cache = caches[f.__qualname__]
cache[key] = [value, datetime.now()]
def wrapper(f):
def cached_f(*args, **kwargs):
if isinstance(make_key_func, list):
key = "|".join([func() for func in make_key_func])
else:
key = make_key_func()
if get_user_status() != "LDAP_USER":
key += "@mask_user=" + request.cookies.get("username", "")
if f.__qualname__ not in caches:
caches[f.__qualname__] = dict()
cache = caches[f.__qualname__]
value = cache.get(key, sentinel)
if value is sentinel:
value = [f(*args, **kwargs), datetime.now()]
cache[key] = value
elif datetime.now() - value[1] > ttl:
value[1] = datetime.now()
ctx = _cv_request.get(None)
if ctx is None:
raise RuntimeError(
"'copy_current_request_context' can only be used when a"
" request context is active, such as in a view function."
)
ctx = ctx.copy()
thread = threading.Thread(target=async_update_task, args=(ctx, key, f, args, kwargs))
thread.start()
return value[0]
cached_f.__name__ = f.__name__
return cached_f
return wrapper
K8S 随机选择 Pod 导致任务乱序
对于同一个榜单来说,策略的 pod 一般情况下只有镜像存在不同。在这种情况下,当资源不满足所有 Pod 资源请求时,K8S 会随机选择一个可以运行的 Pod 将其运行。这就导致,可能先后提交的两个策略,后面的策略对应的 Pod 先占用资源进行运行,进而导致任务乱序。
为了降低这种乱序的情况,和运维的人进行沟通,并进行配合,为每个 pod 分配不同的优先级,先创建的 pod 优先级更高(优先级数值更高)。每次创建一个 submit 的各个 job 对应的 pod,都会创建对应的优先级对象,这个优先级的值为 10000000 - submit-id。从而使得先提交的策略,优先级更高。
服务 Pod 异常重启导致日志丢失
这是因为 Pod 重启后,容器内的文件系统会被重新初始化,之前存储在容器内的日志文件可能会被删除或覆盖。
有时候存在通过 K8S pod event 无法看出重启原因,从而无法分析出重启原因。后来查找资料的时候,发现 K8S API 库请求 pod log 时候,可以配置 previous=True 来获得上一次重启时的日志内容,进而可以分析重启原因。
v1.read_namespaced_pod_log(name, K8S_NAMESPACE, tail_lines=tail_lines, previous=True)
算法提交策略完成时间无目标等待
由于算法人员无法查看 K8S 当前 pod 的运行状态,不知道当前运行到哪里了。通过 submit status,只能看到处于初始化 or 等待资源状态。为了让算法人员使用更舒适,所以和产品讨论,增加一个未完成的 submit 列表。
展示当前未完成的 submit 列表,从而方便算法人员看到当前运行的 submit ID 到了哪里,自己的策略排在什么位置。
后面,自己又在前端嵌入了一个 iframe,链接指向 grafana 页面,展示当前集群的 CPU、GPU、Memory 状态。这个状态是 Pod 申请的资源和。
<iframe src="http://172.27.133.12:30300/d-solo/ca1cb74d-d7a1-4dbd-b2fe-a052d360615a/e8b584-e6ba90-e680bb-e8a788?orgId=1&from={{current_timestamp}}-1&to={{current_timestamp}}&theme=light&panelId=12" width="100%" height="200" frameborder="0"></iframe>
主要计算的逻辑是:正在运行或待处理的 Pod 请求的资源总量与集群可分配资源总量的比值。
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"color": {
"mode": "palette-classic"
},
"fieldMinMax": false,
"unit": "percentunit"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "sum(kube_pod_container_resource_requests{resource=\"cpu\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))/sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"cpu\"})"
},
"properties": [
{
"id": "displayName",
"value": "CPU"
}
]
},
{
"matcher": {
"id": "byName",
"options": "sum(kube_pod_container_resource_requests{resource=\"memory\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))/sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"memory\"})"
},
"properties": [
{
"id": "displayName",
"value": "MEMORY"
}
]
},
{
"matcher": {
"id": "byName",
"options": "(sum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))-sum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"} * on (pod)\n group_left() max by (pod) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ) * on (pod)\n kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\",job=\"kube-state-metrics\"} / on(pod) kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"}) + \nsum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n )) /100)/(sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"nvidia_com_gpu\"})-sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"nvidia_com_gpu\",node=~\"ucloud-wlcb-gpu-080|ucloud-wlcb-gpu-016\"})*0.9)"
},
"properties": [
{
"id": "displayName",
"value": "GPU"
}
]
}
]
},
"gridPos": {
"h": 4,
"w": 18,
"x": 0,
"y": 27
},
"id": 12,
"options": {
"reduceOptions": {
"values": false,
"calcs": [
"lastNotNull"
],
"fields": ""
},
"orientation": "auto",
"textMode": "value_and_name",
"wideLayout": false,
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto"
},
"pluginVersion": "10.2.2",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))/sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"cpu\"})",
"hide": false,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(kube_pod_container_resource_requests{resource=\"memory\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))/sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"memory\"})",
"hide": false,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "(sum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ))-sum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"} * on (pod)\n group_left() max by (pod) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n ) * on (pod)\n kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\",job=\"kube-state-metrics\"} / on(pod) kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"}) + \nsum(kube_pod_container_resource_requests{resource=\"nvidia_com_gpucores\",job=\"kube-state-metrics\"} * on (namespace, pod, cluster)\n group_left() max by (namespace, pod, cluster) (\n (kube_pod_status_phase{phase=~\"Pending|Running\"} == 1)\n )) /100)/(sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"nvidia_com_gpu\"})-sum(kube_node_status_allocatable{job=\"kube-state-metrics\",resource=\"nvidia_com_gpu\",node=~\"ucloud-wlcb-gpu-080|ucloud-wlcb-gpu-016\"})*0.9)",
"hide": false,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "C"
}
],
"title": "资源总览",
"type": "stat"
}榜首服务策略开源化
编辑比赛,将 localStorage → localforage,原因是出现了 overflow。前者很小,一般 5MB,后者大得多,通常可以几百 MB。
10、11 月没有闭卷的时候,算法本地启动服务,导致如果提交多个策略,会使得算法服务同时受到请求。自行 kill 任务。
11、12 月开启闭卷比赛。根据闭卷比赛成绩来进行分配 GPU 机器。
3 月切换断网:断网后,如何加载我自己的大模型、词表、数据等镜像运行需要的大文件?
见算法竞赛场使用 FAQ - 1.1: https://wiki.4paradigm.com/pages/viewpage.action?pageId=140889286
此时遇到了 copilot 子 chart 网络问题
3 月底,modelhub
看到了 5.20 群聊天记录