前端使用 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 群聊天记录