diff --git a/.gitignore b/.gitignore index f9a0a18..5dd8c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ __pycache__/ .venv/ .pytest_cache/ .coverage -htmlcov/ -AGENTS.md \ No newline at end of file +htmlcov/ \ No newline at end of file diff --git a/doc/arch.excalidraw b/doc/arch.excalidraw deleted file mode 100644 index eabf1ab..0000000 --- a/doc/arch.excalidraw +++ /dev/null @@ -1,2133 +0,0 @@ -{ - "type": "excalidraw", - "version": 2, - "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", - "elements": [ - { - "id": "9Ql3xet2wP4Oy9RJ4s-8H", - "type": "rectangle", - "x": 36.00023651123047, - "y": 132.66671752929688, - "width": 201.66666412353516, - "height": 144.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a0", - "roundness": { - "type": 3 - }, - "seed": 1490759950, - "version": 343, - "versionNonce": 480492174, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "Yx_fL1nYHbxINMBHYImpi" - }, - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow" - } - ], - "updated": 1765768777801, - "link": null, - "locked": false - }, - { - "id": "Yx_fL1nYHbxINMBHYImpi", - "type": "text", - "x": 97.75359725952148, - "y": 192.5000457763672, - "width": 78.15994262695312, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a1", - "roundness": null, - "seed": 122704078, - "version": 196, - "versionNonce": 229193934, - "isDeleted": false, - "boundElements": [], - "updated": 1765768777801, - "link": null, - "locked": false, - "text": "desktop", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "9Ql3xet2wP4Oy9RJ4s-8H", - "originalText": "desktop", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "1BW7RJyVw0yjg93vAIlWT", - "type": "rectangle", - "x": 688.5001335144043, - "y": 25.000045776367188, - "width": 159.9999771118164, - "height": 88.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a2", - "roundness": { - "type": 3 - }, - "seed": 929976718, - "version": 607, - "versionNonce": 1421967566, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "E6pCjIGdOvsBKIu6w2tUA" - }, - { - "id": "ye7b-vsIkRFaAtO9UNCf1", - "type": "arrow" - } - ], - "updated": 1765768506683, - "link": null, - "locked": false - }, - { - "id": "E6pCjIGdOvsBKIu6w2tUA", - "type": "text", - "x": 723.7201614379883, - "y": 44.3333740234375, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a3", - "roundness": null, - "seed": 1091104718, - "version": 489, - "versionNonce": 1003055054, - "isDeleted": false, - "boundElements": [], - "updated": 1765768542188, - "link": null, - "locked": false, - "text": "container\n8 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "1BW7RJyVw0yjg93vAIlWT", - "originalText": "container\n8 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "nIO4qa9Aj4LF_WlHt2BNO", - "type": "rectangle", - "x": 326.5000419616699, - "y": 133.6666717529297, - "width": 201.66666412353516, - "height": 144.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a4", - "roundness": { - "type": 3 - }, - "seed": 2137052434, - "version": 248, - "versionNonce": 1543678354, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "xKyNarDpAWsNVlcCC8N-N" - }, - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow" - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow" - }, - { - "id": "5tNZW8jUhEBPa946TLQfX", - "type": "arrow" - } - ], - "updated": 1765768808905, - "link": null, - "locked": false - }, - { - "id": "xKyNarDpAWsNVlcCC8N-N", - "type": "text", - "x": 367.89341735839844, - "y": 143.5, - "width": 118.87991333007812, - "height": 125, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a5", - "roundness": null, - "seed": 711484114, - "version": 158, - "versionNonce": 772410466, - "isDeleted": false, - "boundElements": [], - "updated": 1765770671319, - "link": null, - "locked": false, - "text": "container\nWeb server \n+\nSkyPilot like\nmanager", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "nIO4qa9Aj4LF_WlHt2BNO", - "originalText": "container\nWeb server \n+\nSkyPilot like manager", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow", - "x": 242.66690063476562, - "y": 204.9000457763672, - "width": 78.8331413269043, - "height": 0.9999542236328125, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a6", - "roundness": null, - "seed": 1158070290, - "version": 228, - "versionNonce": 2034953426, - "isDeleted": false, - "boundElements": [], - "updated": 1765768780570, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 78.8331413269043, - 0.9999542236328125 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9Ql3xet2wP4Oy9RJ4s-8H", - "fixedPoint": [ - 1.0247933887424108, - 0.4993087557117625 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "nIO4qa9Aj4LF_WlHt2BNO", - "focus": -0.008594144923853504, - "gap": 14.333358764648438, - "fixedPoint": [ - 0.48677675845282387, - 1.0990785237732301 - ] - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "aBQ9npStDj077n0B7_JZ3", - "type": "rectangle", - "x": 690.000072479248, - "y": 147.6666717529297, - "width": 159.9999771118164, - "height": 88.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a7", - "roundness": { - "type": 3 - }, - "seed": 1963206674, - "version": 685, - "versionNonce": 329674770, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "dGa3VHynwpnzguZ5kDJTr" - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow" - }, - { - "id": "-w3fkuAvGpZM4OdIK7N85", - "type": "arrow" - } - ], - "updated": 1765768511284, - "link": null, - "locked": false - }, - { - "id": "dGa3VHynwpnzguZ5kDJTr", - "type": "text", - "x": 725.220100402832, - "y": 167, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a8", - "roundness": null, - "seed": 1445549522, - "version": 568, - "versionNonce": 406828686, - "isDeleted": false, - "boundElements": [], - "updated": 1765768553134, - "link": null, - "locked": false, - "text": "container\n8 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "aBQ9npStDj077n0B7_JZ3", - "originalText": "container\n8 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "Yba-9HedLPKxDUD1wthap", - "type": "rectangle", - "x": 695.3333854675293, - "y": 274.33338928222656, - "width": 159.9999771118164, - "height": 88.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a9", - "roundness": { - "type": 3 - }, - "seed": 259691346, - "version": 730, - "versionNonce": 1394721042, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "NfiPIJkEoh7Nlnz6VVp0Y" - }, - { - "id": "lQj9EyeS508UiTeYSg02r", - "type": "arrow" - } - ], - "updated": 1765768515918, - "link": null, - "locked": false - }, - { - "id": "NfiPIJkEoh7Nlnz6VVp0Y", - "type": "text", - "x": 730.5534133911133, - "y": 293.6667175292969, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aA", - "roundness": null, - "seed": 87054610, - "version": 614, - "versionNonce": 2098855246, - "isDeleted": false, - "boundElements": [], - "updated": 1765768564520, - "link": null, - "locked": false, - "text": "container\n8 X H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Yba-9HedLPKxDUD1wthap", - "originalText": "container\n8 X H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow", - "x": 533.1667060852051, - "y": 205.9, - "width": 136.83329391479492, - "height": 0.7666870117187443, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aC", - "roundness": null, - "seed": 990553042, - "version": 41, - "versionNonce": 821443154, - "isDeleted": false, - "boundElements": [], - "updated": 1765768401701, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 136.83329391479492, - 0.7666870117187443 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "nIO4qa9Aj4LF_WlHt2BNO", - "fixedPoint": [ - 1.0247933887424108, - 0.4993087557117625 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "aBQ9npStDj077n0B7_JZ3", - "fixedPoint": [ - -0.12500047087676108, - 0.66541378226761 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "z4vmx2a_K8omTNuLcVRld", - "type": "diamond", - "x": 910.3333740234375, - "y": 154.66668701171875, - "width": 96.33331298828125, - "height": 120, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aF", - "roundness": { - "type": 2 - }, - "seed": 2059710478, - "version": 40, - "versionNonce": 573449362, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "_CVsRhVz21ru5fuqV5vp7" - }, - { - "id": "ye7b-vsIkRFaAtO9UNCf1", - "type": "arrow" - }, - { - "id": "-w3fkuAvGpZM4OdIK7N85", - "type": "arrow" - }, - { - "id": "lQj9EyeS508UiTeYSg02r", - "type": "arrow" - } - ], - "updated": 1765768515919, - "link": null, - "locked": false - }, - { - "id": "_CVsRhVz21ru5fuqV5vp7", - "type": "text", - "x": 945.3567123413086, - "y": 189.66668701171875, - "width": 26.119979858398438, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aG", - "roundness": null, - "seed": 104641810, - "version": 10, - "versionNonce": 121397710, - "isDeleted": false, - "boundElements": [], - "updated": 1765768473262, - "link": null, - "locked": false, - "text": "IB\nsw", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "z4vmx2a_K8omTNuLcVRld", - "originalText": "IB sw", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "ye7b-vsIkRFaAtO9UNCf1", - "type": "arrow", - "x": 853.5001106262207, - "y": 69.2333740234375, - "width": 104.8999198913574, - "height": 80.43331298828124, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aH", - "roundness": null, - "seed": 1146593042, - "version": 30, - "versionNonce": 45365006, - "isDeleted": false, - "boundElements": [], - "updated": 1765768506683, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 104.8999198913574, - 0 - ], - [ - 104.8999198913574, - 80.43331298828124 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "1BW7RJyVw0yjg93vAIlWT", - "fixedPoint": [ - 1.031250004470349, - 0.4988721803217357 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "z4vmx2a_K8omTNuLcVRld", - "fixedPoint": [ - 0.49896193749702983, - -0.041666666666666664 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "-w3fkuAvGpZM4OdIK7N85", - "type": "arrow", - "x": 855.0000495910645, - "y": 191.9, - "width": 50.33332443237305, - "height": 22.66668701171875, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aI", - "roundness": null, - "seed": 1001105870, - "version": 16, - "versionNonce": 2034408914, - "isDeleted": false, - "boundElements": [], - "updated": 1765768511284, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 25.166662216186523, - 0 - ], - [ - 25.166662216186523, - 22.66668701171875 - ], - [ - 50.33332443237305, - 22.66668701171875 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "aBQ9npStDj077n0B7_JZ3", - "fixedPoint": [ - 1.031250004470349, - 0.4988721803217357 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "z4vmx2a_K8omTNuLcVRld", - "fixedPoint": [ - -0.05190312514849603, - 0.4991666666666667 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "lQj9EyeS508UiTeYSg02r", - "type": "arrow", - "x": 860.3333625793457, - "y": 318.56671752929685, - "width": 98.0666679382324, - "height": 38.9000305175781, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aJ", - "roundness": null, - "seed": 1380767506, - "version": 47, - "versionNonce": 791400146, - "isDeleted": false, - "boundElements": [], - "updated": 1765768515919, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 98.0666679382324, - 0 - ], - [ - 98.0666679382324, - -38.9000305175781 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "Yba-9HedLPKxDUD1wthap", - "fixedPoint": [ - 1.031250004470349, - 0.49887218032173536 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "z4vmx2a_K8omTNuLcVRld", - "fixedPoint": [ - 0.49896193749702983, - 1.0416666666666667 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "Rsu2l5FbjC-YeEsUJuZ8e", - "type": "rectangle", - "x": 699.4999694824219, - "y": 411.3334045410156, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aO", - "roundness": { - "type": 3 - }, - "seed": 1067651918, - "version": 127, - "versionNonce": 1654930766, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "PP3Q89REJw99Q6rQVaV55" - } - ], - "updated": 1765768653875, - "link": null, - "locked": false - }, - { - "id": "PP3Q89REJw99Q6rQVaV55", - "type": "text", - "x": 733.2200088500977, - "y": 431.6667175292969, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aP", - "roundness": null, - "seed": 1891596686, - "version": 78, - "versionNonce": 772498318, - "isDeleted": false, - "boundElements": [], - "updated": 1765768653875, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Rsu2l5FbjC-YeEsUJuZ8e", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "AZXoJzGVX5uoKshaeKQek", - "type": "rectangle", - "x": 713.5000305175781, - "y": 419.3333435058594, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aQ", - "roundness": { - "type": 3 - }, - "seed": 2030401490, - "version": 163, - "versionNonce": 2142662354, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "ydc8eycBIHMLutlmYkUH8" - } - ], - "updated": 1765768657308, - "link": null, - "locked": false - }, - { - "id": "ydc8eycBIHMLutlmYkUH8", - "type": "text", - "x": 747.2200698852539, - "y": 439.6666564941406, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aR", - "roundness": null, - "seed": 887795090, - "version": 114, - "versionNonce": 1214563474, - "isDeleted": false, - "boundElements": [], - "updated": 1765768657308, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "AZXoJzGVX5uoKshaeKQek", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "cd8duVnreXCTzqLOws63b", - "type": "rectangle", - "x": 730.8333435058594, - "y": 428.0000305175781, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aS", - "roundness": { - "type": 3 - }, - "seed": 743406094, - "version": 193, - "versionNonce": 1116698638, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "N1-LLyWwsryNzbp5IxXA5" - } - ], - "updated": 1765768660341, - "link": null, - "locked": false - }, - { - "id": "N1-LLyWwsryNzbp5IxXA5", - "type": "text", - "x": 764.5533828735352, - "y": 448.3333435058594, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aT", - "roundness": null, - "seed": 1085343822, - "version": 144, - "versionNonce": 1245791822, - "isDeleted": false, - "boundElements": [], - "updated": 1765768660341, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "cd8duVnreXCTzqLOws63b", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "N98V2SE6XUuoHWnKGx7Nf", - "type": "rectangle", - "x": 700.1666564941406, - "y": 531.0000915527344, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aU", - "roundness": { - "type": 3 - }, - "seed": 528387662, - "version": 211, - "versionNonce": 1184314770, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "ARZv73nqYLBG1llkZAoCX" - }, - { - "id": "5tNZW8jUhEBPa946TLQfX", - "type": "arrow" - } - ], - "updated": 1765768742972, - "link": null, - "locked": false - }, - { - "id": "ARZv73nqYLBG1llkZAoCX", - "type": "text", - "x": 733.8866958618164, - "y": 551.3334045410156, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aV", - "roundness": null, - "seed": 1567855758, - "version": 161, - "versionNonce": 1767508046, - "isDeleted": false, - "boundElements": [], - "updated": 1765768703417, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "N98V2SE6XUuoHWnKGx7Nf", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "i2ZxkoxnCoyAnZe4KBHsQ", - "type": "rectangle", - "x": 714.1667175292969, - "y": 539.0000305175781, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aW", - "roundness": { - "type": 3 - }, - "seed": 460182222, - "version": 246, - "versionNonce": 298831502, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "L4x3Y2L00Gw6130G6lU9W" - } - ], - "updated": 1765768703417, - "link": null, - "locked": false - }, - { - "id": "L4x3Y2L00Gw6130G6lU9W", - "type": "text", - "x": 747.8867568969727, - "y": 559.3333435058594, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aX", - "roundness": null, - "seed": 335956238, - "version": 197, - "versionNonce": 741526734, - "isDeleted": false, - "boundElements": [], - "updated": 1765768703417, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "i2ZxkoxnCoyAnZe4KBHsQ", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "RsHyOceETLPpOF-hqhu84", - "type": "rectangle", - "x": 731.5000305175781, - "y": 547.6667175292969, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aY", - "roundness": { - "type": 3 - }, - "seed": 1186330446, - "version": 276, - "versionNonce": 1981547278, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "fIcwnXZ9pveKZfR6TRcaa" - } - ], - "updated": 1765768703417, - "link": null, - "locked": false - }, - { - "id": "fIcwnXZ9pveKZfR6TRcaa", - "type": "text", - "x": 765.2200698852539, - "y": 568.0000305175781, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aZ", - "roundness": null, - "seed": 1540742542, - "version": 229, - "versionNonce": 302281038, - "isDeleted": false, - "boundElements": [], - "updated": 1765768703417, - "link": null, - "locked": false, - "text": "container\n2 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "RsHyOceETLPpOF-hqhu84", - "originalText": "container\n2 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "cti4IlX8pCaQ3LQzTJVGl", - "type": "rectangle", - "x": 902.8333435058594, - "y": 416.3334655761719, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aa", - "roundness": { - "type": 3 - }, - "seed": 1357303054, - "version": 271, - "versionNonce": 216485262, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "2kUMneeoOFXj5_Ooa3RKS" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "2kUMneeoOFXj5_Ooa3RKS", - "type": "text", - "x": 936.5533828735352, - "y": 436.6667785644531, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ab", - "roundness": null, - "seed": 484834126, - "version": 222, - "versionNonce": 1979870158, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "cti4IlX8pCaQ3LQzTJVGl", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "GKmziFZzlbsnmMSSs5SBZ", - "type": "rectangle", - "x": 916.8334045410156, - "y": 424.3334045410156, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ac", - "roundness": { - "type": 3 - }, - "seed": 1969578382, - "version": 307, - "versionNonce": 1117766158, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "xlfGTET36AVVDlpQgBRCD" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "xlfGTET36AVVDlpQgBRCD", - "type": "text", - "x": 950.5534439086914, - "y": 444.6667175292969, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ad", - "roundness": null, - "seed": 1302200270, - "version": 258, - "versionNonce": 901271630, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "GKmziFZzlbsnmMSSs5SBZ", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "C2Rakvrst2HdxFq6yB2oR", - "type": "rectangle", - "x": 934.1667175292969, - "y": 433.0000915527344, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ae", - "roundness": { - "type": 3 - }, - "seed": 1235801614, - "version": 337, - "versionNonce": 1260721806, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "WjewWchfoQAb_fO4XwVO4" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "WjewWchfoQAb_fO4XwVO4", - "type": "text", - "x": 967.8867568969727, - "y": 453.3334045410156, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "af", - "roundness": null, - "seed": 578741326, - "version": 290, - "versionNonce": 463410382, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n4 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "C2Rakvrst2HdxFq6yB2oR", - "originalText": "container\n4 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "GQAuJzEnRE7cwh01SCj2r", - "type": "rectangle", - "x": 905.4999694824219, - "y": 531.6669006347656, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ag", - "roundness": { - "type": 3 - }, - "seed": 1259957198, - "version": 294, - "versionNonce": 1086278414, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "05b5QF2wFlFPzlyknjo-l" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "05b5QF2wFlFPzlyknjo-l", - "type": "text", - "x": 939.2200088500977, - "y": 552.0002136230469, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ah", - "roundness": null, - "seed": 1557578254, - "version": 245, - "versionNonce": 956217678, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "GQAuJzEnRE7cwh01SCj2r", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "trV5f691bxKhXCvtAsFJp", - "type": "rectangle", - "x": 919.5000305175781, - "y": 539.6668395996094, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ai", - "roundness": { - "type": 3 - }, - "seed": 1896829006, - "version": 330, - "versionNonce": 1270243214, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "zDLI66UPB74U0-pceROrx" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "zDLI66UPB74U0-pceROrx", - "type": "text", - "x": 953.2200698852539, - "y": 560.0001525878906, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aj", - "roundness": null, - "seed": 60561038, - "version": 281, - "versionNonce": 1830763982, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n1 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "trV5f691bxKhXCvtAsFJp", - "originalText": "container\n1 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "zjH8FeFL_DKnnms6pEZGl", - "type": "rectangle", - "x": 936.8333435058594, - "y": 548.3335266113281, - "width": 157, - "height": 90.6666259765625, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ak", - "roundness": { - "type": 3 - }, - "seed": 758518990, - "version": 360, - "versionNonce": 575820814, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "pEXkPy2D2mjJ4et_R9S-G" - } - ], - "updated": 1765768729712, - "link": null, - "locked": false - }, - { - "id": "pEXkPy2D2mjJ4et_R9S-G", - "type": "text", - "x": 970.5533828735352, - "y": 568.6668395996094, - "width": 89.55992126464844, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "al", - "roundness": null, - "seed": 125192974, - "version": 315, - "versionNonce": 1699258958, - "isDeleted": false, - "boundElements": [], - "updated": 1765768729712, - "link": null, - "locked": false, - "text": "container\n8 x H100", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "zjH8FeFL_DKnnms6pEZGl", - "originalText": "container\n8 x H100", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "5tNZW8jUhEBPa946TLQfX", - "type": "arrow", - "x": 531.6771272112763, - "y": 258.6784160210832, - "width": 152.98952928286428, - "height": 303.98824047305743, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "am", - "roundness": null, - "seed": 1864421970, - "version": 209, - "versionNonce": 911403858, - "isDeleted": false, - "boundElements": [], - "updated": 1765768746717, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 46.32296434145803, - 0 - ], - [ - 46.32296434145803, - 303.98824047305743 - ], - [ - 152.98952928286428, - 303.98824047305743 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "nIO4qa9Aj4LF_WlHt2BNO", - "fixedPoint": [ - 1.0174070471256513, - 0.864136541879758 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "N98V2SE6XUuoHWnKGx7Nf", - "fixedPoint": [ - -0.09872611464968153, - 0.3492637406579144 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": [ - { - "index": 2, - "start": [ - 46.32296434145803, - 0 - ], - "end": [ - 46.32296434145803, - 303.98824047305743 - ] - } - ], - "startIsSpecial": false, - "endIsSpecial": false - }, - { - "id": "L5L-dHWhO7QDfKsx-auzO", - "type": "line", - "x": 679.6486400773596, - "y": 68.05050870475259, - "width": 6.218773981147137, - "height": 125.20467479135232, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aq", - "roundness": { - "type": 2 - }, - "seed": 1924412686, - "version": 48, - "versionNonce": 2072832782, - "isDeleted": false, - "boundElements": [], - "updated": 1765768839277, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -6.218773981147137, - 125.20467479135232 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "L7fPHZgtyOHIM1-qa1gD7", - "type": "line", - "x": 673.0152305556237, - "y": 218.13027942069357, - "width": 15.339693095341886, - "height": 102.8170125464543, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ar", - "roundness": { - "type": 2 - }, - "seed": 1982802578, - "version": 50, - "versionNonce": 247804498, - "isDeleted": false, - "boundElements": [], - "updated": 1765768879487, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 15.339693095341886, - 102.8170125464543 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "f7LL01k78v1et3Xn56EDr", - "type": "text", - "x": 763.394771052552, - "y": -27.304050977093425, - "width": 161.1998748779297, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "as", - "roundness": null, - "seed": 1515956750, - "version": 39, - "versionNonce": 727326030, - "isDeleted": false, - "boundElements": [], - "updated": 1765768936418, - "link": null, - "locked": false, - "text": "multi-nodes task", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "multi-nodes task", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "tuQoRzheGl5PV5c38jxd1", - "type": "text", - "x": 812.567405914188, - "y": 658.3570252482072, - "width": 158.2798614501953, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "at", - "roundness": null, - "seed": 609937742, - "version": 89, - "versionNonce": 2106648658, - "isDeleted": false, - "boundElements": [], - "updated": 1765768928899, - "link": null, - "locked": false, - "text": "single-node task", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "single-node task", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "atIqduRWNX97Sp6yKe4hj", - "type": "rectangle", - "x": 301.8035086475226, - "y": -105.80058630360065, - "width": 819.6344107151963, - "height": 1019.8789708645193, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aw", - "roundness": { - "type": 3 - }, - "seed": 311282494, - "version": 157, - "versionNonce": 601142206, - "isDeleted": false, - "boundElements": null, - "updated": 1765770973019, - "link": null, - "locked": false - }, - { - "id": "k6SXmpRXbeQSbRrXG_o6a", - "type": "text", - "x": 691.5133194951555, - "y": -158.86744495726174, - "width": 111.85990905761719, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ax", - "roundness": null, - "seed": 570087970, - "version": 13, - "versionNonce": 2027488254, - "isDeleted": false, - "boundElements": null, - "updated": 1765770695453, - "link": null, - "locked": false, - "text": "K8S cluster", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "K8S cluster", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "bhihBvlxof1k0U05Mpyjq", - "type": "ellipse", - "x": 397.77986981471486, - "y": 830.7468511699307, - "width": 659.6046775421883, - "height": 43.94597749585046, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "az", - "roundness": { - "type": 2 - }, - "seed": 718418402, - "version": 98, - "versionNonce": 948231742, - "isDeleted": false, - "boundElements": [], - "updated": 1765771067870, - "link": null, - "locked": false - }, - { - "id": "V2T98L5fdxwL266Zl6nbF", - "type": "rectangle", - "x": 398.8163827534184, - "y": 773.1195329591723, - "width": 662.0921112218787, - "height": 80.4294262143244, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b00", - "roundness": null, - "seed": 1034220734, - "version": 126, - "versionNonce": 721447970, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "iqdjGP3so6IznFUVxyJ5u" - } - ], - "updated": 1765771080130, - "link": null, - "locked": false - }, - { - "id": "iqdjGP3so6IznFUVxyJ5u", - "type": "text", - "x": 568.7425653174828, - "y": 800.8342460663345, - "width": 322.23974609375, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b00V", - "roundness": null, - "seed": 204793534, - "version": 39, - "versionNonce": 2093989474, - "isDeleted": false, - "boundElements": null, - "updated": 1765771090688, - "link": null, - "locked": false, - "text": "shared storage for all containers", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "V2T98L5fdxwL266Zl6nbF", - "originalText": "shared storage for all containers", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "vJ4ppQKXnitvRup82_d2B", - "type": "ellipse", - "x": 397.9871875850093, - "y": 749.9027514586334, - "width": 659.6046775421883, - "height": 43.94597749585046, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffffff", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b01", - "roundness": { - "type": 2 - }, - "seed": 1318325922, - "version": 68, - "versionNonce": 818986238, - "isDeleted": false, - "boundElements": null, - "updated": 1765771024715, - "link": null, - "locked": false - } - ], - "appState": { - "gridSize": 20, - "gridStep": 5, - "gridModeEnabled": false, - "viewBackgroundColor": "#ffffff" - }, - "files": {} -} \ No newline at end of file diff --git a/specs/assets/arch.png b/doc/assets/arch.png similarity index 100% rename from specs/assets/arch.png rename to doc/assets/arch.png diff --git a/specs/assets/layout.png b/doc/assets/layout.png similarity index 100% rename from specs/assets/layout.png rename to doc/assets/layout.png diff --git a/specs/assets/networking.png b/doc/assets/networking.png similarity index 100% rename from specs/assets/networking.png rename to doc/assets/networking.png diff --git a/specs/assets/node_bootstrap.png b/doc/assets/node_bootstrap.png similarity index 100% rename from specs/assets/node_bootstrap.png rename to doc/assets/node_bootstrap.png diff --git a/specs/assets/node_onboarding.png b/doc/assets/node_onboarding.png similarity index 100% rename from specs/assets/node_onboarding.png rename to doc/assets/node_onboarding.png diff --git a/specs/assets/scheduling.png b/doc/assets/scheduling.png similarity index 100% rename from specs/assets/scheduling.png rename to doc/assets/scheduling.png diff --git a/specs/assets/task_submit.png b/doc/assets/task_submit.png similarity index 100% rename from specs/assets/task_submit.png rename to doc/assets/task_submit.png diff --git a/doc/hl_design.md b/doc/hl_design.md deleted file mode 100644 index fd19aa7..0000000 --- a/doc/hl_design.md +++ /dev/null @@ -1,279 +0,0 @@ -# AI Infra 训练平台建设方案 - -## 1. 愿景与目标 - -### 1.1 愿景 - -构建一套**端到端的智能化AI训练平台**,将分散的训练框架、资源调度、监控运维、数据管理能力整合为统一的标准化流水线,让大模型训练算法团队**专注于模型创新而非基础设施运维**,同时与现有运维智能体深度协同,实现训练任务的智能化运维闭环。 - -### 1.2 核心目标 - -| 目标维度 | 描述 | -|---------|------| -| **效率提升** | 训练任务从准备到启动时间缩短 70%,故障恢复时间缩短 50% | -| **标准化** | 建立统一的训练流程规范,消除"人人一套环境"的混乱局面 | -| **可观测性** | 全链路监控覆盖,训练状态、资源利用、异常事件一目了然 | -| **智能运维** | 与运维智能体对接,实现断训自动分析、故障智能诊断 | - - ---- - -## 2. 整体架构概览 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ 用户交互层 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Web 前端 │ │ CLI 工具 │ │ API 接口 │ │ -│ │ (任务提交) │ │ (高级用户) │ │ (自动化集成) │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────────┐ -│ 平台服务层 │ -│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ │ 任务调度 │ │ 数据管理 │ │ 模型管理 │ │ -│ │ (SkyPilot) │ │(schema/dataset)│ │ (版本/产物) │ │ -│ └───────────────┘ └───────────────┘ └───────────────┘ │ -│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ │ 镜像管理 │ │ 指标追踪 │ │ 日志中心 │ │ -│ │(Local Registry)│ │ (W&B) │ │ (集中采集) │ │ -│ └───────────────┘ └───────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────────┐ -│ 智能运维层 │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 运维智能体对接 │ │ -│ │ • 断训自动分析 • 故障根因定位 • 资源利用率优化建议 │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────────┐ -│ 基础设施层 │ -│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ │ Kubernetes │ │ GPU 集群 │ │ 分布式存储 │ │ -│ │ (容器编排) │ │ H20/A6000/H100│ │ (JuiceFS) │ │ -│ └───────────────┘ └───────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. 用户故事 - -### 3.1 算法工程师视角 - -> **作为**一名算法工程师, -> **我希望**通过简单的界面配置就能提交一个多节点 RLHF 训练任务, -> **以便于**我可以专注于模型和数据本身,而不是花大量时间在环境配置和资源协调上。 - -**验收标准:** -- [ ] 在 Web 界面上选择数据集、模型、训练配置 -- [ ] 一键提交后,系统自动完成资源分配、镜像拉取、任务启动 -- [ ] 实时查看训练进度曲线和关键指标 - ---- - -> **作为**一名算法工程师, -> **我希望**训练中断时能快速定位问题原因, -> **以便于**减少排查时间,尽快恢复训练。 - -**验收标准:** -- [ ] 系统自动检测训练中断事件 -- [ ] 智能体自动分析中断原因(OOM、网络故障、硬件异常等) -- [ ] 提供可操作的恢复建议 - ---- - -> **作为**一名算法工程师, -> **我希望**启动一个 Notebook 环境调试代码. -> **以便于**小规模试跑训练,测试训练数据集、调整模型参数 - -**验收标准:** -- [ ] Notebook容器启动速度 -- [ ] 开发容器内置依赖包完善度,按照新包 - ---- - -### 3.2 团队负责人视角 - -> **作为**团队负责人, -> **我希望**能够看到所有训练任务的整体资源利用情况, -> **以便于**合理规划算力资源,识别资源浪费。 - -**验收标准:** -- [ ] 仪表盘展示各集群 GPU 利用率趋势 -- [ ] 任务队列可视化,等待/运行/完成状态一目了然 -- [ ] 资源使用报表按项目/用户统计 - ---- - -### 3.3 运维工程师视角 - -> **作为**运维工程师, -> **我希望**训练任务的监控数据能自动接入现有运维系统, -> **以便于**统一管理,减少割裂的监控工具。 - -**验收标准:** -- [ ] 训练任务指标自动推送到运维智能体 -- [ ] 异常告警自动触发智能体分析流程 -- [ ] 与现有运维系统数据互通 - ---- - -## 4. 里程碑规划 - -### 里程碑总览 - -``` - M1 M2 M3 M4 - │ │ │ │ -────●──────────────────●──────────────────●──────────────────●────────▶ - │ │ │ │ - 基础设施就绪 训练流水线上线 监控运维闭环 智能化运维 -``` - ---- - -### M1: 基础设施就绪 - -**目标:** 完成底层平台搭建,具备运行训练任务的基础能力 - -| 交付物 | 说明 | -|-------|------| -| K8S 集群 | H20 集群上部署 Kubernetes,支持 GPU 调度 | -| 本地 Registry | 内网镜像仓库,解决镜像拉取问题 | -| JuiceFS/MinIO | 分布式存储,数据集和模型 checkpoint 持久化 | -| 基础镜像 | veRL 训练镜像,预置常用依赖 | - -**关键验证点:** -- 能够手动在 K8S 上启动单节点训练任务 -- 数据从 JuiceFS 正常读写 -- 镜像从本地 Registry 正常拉取 -- 引入 Volcano 或 Kueue,并配置 Gang Scheduling 策略,实现All-or-Nothing 的资源分配 -- 确认JuiceFS 的本地 SSD 缓存策略,在其中一台机器部署MiniIO单节点,另外两台机器上部署JuiceFS client -- 网络通信支持 RoCE/InfiniBand -- Notebook 交互式开发环境 - ---- - -### M2: 训练流水线上线 - -**目标:** 用户可通过前端提交和管理训练任务 - -| 交付物 | 说明 | -|-------|------| -| SkyPilot 集成 | 任务调度与资源编排 | -| W&B 本地服务 | 训练指标追踪与可视化 | -| 任务管理前端 | 数据上传、任务提交、进度查看、日志查看 | -| 数据管理模块 | 支持从 HuggingFace 链接或 FTP 导入数据集 | - -**关键验证点:** -- 端到端完成一次多节点 SFT 训练 -- 通过前端提交任务,查看 W&B 训练曲线 -- 训练日志完整保存并可查询 -- 多租户、项目制配额管理功能 - ---- - -### M3: 监控运维闭环 - -**目标:** 实现任务全生命周期监控,与运维智能体初步对接 - -| 交付物 | 说明 | -|-------|------| -| 资源监控 | GPU 利用率、显存、网络带宽实时采集 | -| 日志采集 | 训练日志集中存储,支持检索 | -| 智能体对接 | 断训事件自动推送,触发智能体分析 | -| 告警机制 | 异常状态(OOM、任务卡死等)自动告警 | - -**关键验证点:** -- 训练任务异常时,5 分钟内收到告警 -- 断训事件自动生成分析报告 -- Grafana 仪表盘展示集群整体健康状态 -- sidecar方式部署 DCGM Exporter 来获取细粒度指标,自动采集到Prometheus -- 断训时“保留现场”机制,供人工/智能体排查介入 - ---- - -### M4: 智能化运维 - -**目标:** 深度整合运维智能体,实现智能调度与自愈 - -| 交付物 | 说明 | -|-------|------| -| 故障自愈 | 常见故障自动处理(如重新调度到健康节点) | -| 智能调度 | 基于历史数据优化任务资源分配 | -| 根因分析 | 复杂故障场景的深度分析能力 | -| 容量预测 | 基于任务趋势预测算力需求 | - ---- - -## 5. 约束与风险 - -### 5.1 已知约束 - -| 约束项 | 影响 | 应对策略 | -|-------|------|---------| -| **内网环境** | 无法直接访问HF/Dockerhub/Github资源(模型、数据集) | 本地 Registry + 数据导入工具 | -| **算力平台限制** | 现有平台调度能力有限 | 引入 SkyPilot 作为上层调度 | -| **数据持久化** | 需要可靠的分布式存储 | JuiceFS + MinIO 方案 | - -### 5.2 潜在风险 - -| 风险 | 可能性 | 影响 | 缓解措施 | -|-----|-------|------|---------| -| K8S 与现有系统集成复杂 | 中 | 高 | 先在 H20 集群小范围验证 | -| 智能体接口适配工作量大 | 中 | 中 | 早期明确接口规范,持续对齐 | -| 用户习惯迁移阻力 | 低 | 中 | 渐进式推广,保留手动模式 | - ---- - -## 6. 资源与依赖 - -### 6.1 硬件资源 - -| 集群 | 配置 | 用途 | -|-----|------|------| -| H20 集群 | 2 节点 × 8 卡 = 16 卡 | 主力训练集群,首期部署目标 | -| A6000 集群 | 2 节点 × 4 卡 = 8 卡 | 开发测试、小规模实验 | -| H100 集群 | 多节点 | 目前仅提供容器方式,不确定能否提供KubeConfig接入,大规模训练 | - -### 6.2 外部依赖 - -| 依赖项 | 状态 | 负责方 | -|-------|------|-------| -| yd运维智能体接口 | 已有基础 | | -| argus运维系统 | 已有 | 运维团队 | - ---- - -## 7. 成功标准 - -### 阶段一完成标准(M1 + M2) - -- [ ] 算法工程师可通过 Web 界面完成 SFT/RLHF 训练全流程 -- [ ] 任务提交到开始训练时间 < 10 分钟 -- [ ] 训练指标实时可视化,延迟 < 1 分钟 -- [ ] 至少完成 3 个实际项目的验证使用 - -### 阶段二完成标准(M3 + M4) - -- [ ] 断训事件 100% 自动检测并推送智能体 -- [ ] 常见故障(OOM、节点失联)自动生成分析报告 -- [ ] GPU 整体利用率提升 20%(通过更好的调度) -- [ ] 平均故障恢复时间(MTTR)缩短 50% - ---- - -## 附录:关键技术选型 - -| 领域 | 选型 | 选型理由 | -|-----|------|---------| -| 容器编排 | Kubernetes | 业界标准,生态成熟 | -| 任务调度 | SkyPilot | 专为 ML 场景设计,支持多集群 | -| 分布式存储 | JuiceFS + MinIO | 兼容 POSIX,适合训练场景 | -| 实验追踪 | W&B (自部署) | 功能完善,团队已有使用经验 | -| 镜像仓库 | Harbor / Registry | 内网环境必需 | -| 训练框架 | veRL / Megatron | 支持 RLHF,与现有工作对齐 | diff --git a/specs/hl_design_v2.md b/doc/hl_design_v2.md similarity index 100% rename from specs/hl_design_v2.md rename to doc/hl_design_v2.md diff --git a/doc/requirements.md b/doc/requirements.md deleted file mode 100644 index c2734fb..0000000 --- a/doc/requirements.md +++ /dev/null @@ -1,18 +0,0 @@ - -目标:设计一套AI任务调度和管理系统 -环境:在K8S集群(商业化封装的算力平台)上运行若干个GPU容器和CPU容器 -约束:CPU容器可以暴露端口供desktop访问,CPU容器可以通过ssh访问GPU容器,容器的生命周期在单独的算力平台控制台上管理;所有容器 - 有共享的NFS文件系统 -目标:在CPU容器上部署一个web server 向desktop提供服务,后端运行一个类似skypilot的程序,能够注册和管理GPU容器实例进来。 - - 用户在desktop上通过网站提交执行模型训练的任务,任务管理程序会将任务加入调度队列,等资源池有空闲资源的时候,就会调度任务到对应的GPU节点上执行任务。任务有单节点任务,和多节点任务两类,预先的节点也提前配置好了,多节点任务的容器之间是专门 - 走的IB网络。 -任务:完成技术选型,主要是普通的skypilot不支持现成的ssh节点管理。你调研是否有节点满足需求的任务调度管理方案。 -补充:运行的任务一般是 verl / skyRL 强化学习训练框架,以及llamafactory 框架等。GPU节点容器假设是已经预先安装好了对应的环境依赖,任务管理器只需要调度和查看任务执行情况,还有监控任务是否健康,有没有任务中断。 -未来规划: - - 统一数据集管理,包括模型库、数据集库 - - 统一日志采集&规范 - - 统一容器GPU/CPU指标采集 - - 统一的 weight & bias 指标服务器 -结构图:见附件 - - diff --git a/doc/storage_deployment.md b/doc/storage_deployment.md deleted file mode 100644 index 4bf2589..0000000 --- a/doc/storage_deployment.md +++ /dev/null @@ -1,315 +0,0 @@ -# 关于MinIO + JuiceFS 在H20部署 - -这是一个架构设计的关键决策点。在 **H20 节点运行 K3s** 的前提下,我建议采取 **“存储组件独立部署(Outside),客户端插件化接入(Inside)”** 的混合策略。 - -简单来说:**MinIO 和 Redis 跑在 K3s 外面(用 Docker),JuiceFS Client 通过 CSI 跑在 K3s 里面。** - -以下是详细的架构拆解和理由: - -### 1\. 架构图解 - -```mermaid -graph TD - subgraph "物理层 / Host OS (H20 Node)" - NVMe[NVMe SSD (物理盘)] - end - - subgraph "K3s 集群 (容器层)" - Pod[训练任务 Pod] - CSI[JuiceFS CSI Driver (DaemonSet)] - - Pod -- "PVC 挂载" --> CSI - end - - subgraph "独立 Docker 容器 (非 K3s)" - Redis[Redis 容器] - MinIO[MinIO 容器] - end - - %% 关键数据流 - CSI -- "1. 读写缓存" --> NVMe - CSI -- "2. 网络 IO" --> MinIO - CSI -- "3. 元数据 IO" --> Redis - - %% 避免环路 - MinIO -. "数据持久化" .- NVMe -``` - ------ - -### 2\. 组件部署位置详解 - -#### A. MinIO & Redis:建议 **独立于 K3s 部署 (Outside)** - -**方式**:直接在宿主机(Host OS)上使用 `docker run` 或 `docker-compose` 启动,或者使用 `systemd` 管理。 - -**理由**: - -1. **爆炸半径隔离(最重要)**:AI 实验初期,K3s 集群可能会因为 CNI 网络插件配置错误、Device Plugin 崩溃、或者各种误操作导致集群不可用(Crash)。如果存储后端(MinIO)跑在 K3s 里,K3s 一挂,你的数据就“锁”在里面了,连日志都拷不出来。 -2. **资源竞争保护**:K8s 的调度器虽然有 QoS,但在只有 2 个节点的情况下,H20 的高负载很容易导致控制面不稳。把 MinIO 独立出来,保证存储服务的 CPU/内存资源不受 K3s 内部 Pod 挤压。 -3. **网络直通**:宿主机直接部署 MinIO,使用的是 `host` 网络,少了一层 K8s Service/Ingress 转发,对于大带宽吞吐稍微有一点点性能优势。 - -#### B. JuiceFS Client:建议 **集成在 K3s 内部 (Inside)** - -**方式**:使用 **JuiceFS CSI Driver**。 - -**理由**: - -1. **符合“平台化”愿景**:你的目标是做一个 "AI Infra 平台"。对于上层用户(算法工程师),他们不应该关心宿主机挂载了什么目录。他们提交的 YAML 只需要写: - ```yaml - volumes: - - name: data - persistentVolumeClaim: - claimName: pvc-juicefs - ``` - 这是标准的云原生体验。 -2. **权限与隔离**:CSI Driver 可以更好地处理容器内的挂载点权限问题。如果手动在宿主机 `mount` 然后用 `hostPath` 映射进容器,经常会遇到 `Permission Denied` 或者 `root` 用户归属问题,非常头疼。 -3. **自动缓存管理**:CSI Driver 可以通过配置 `StorageClass`,自动帮你处理缓存路径的挂载和清理。 - ------ - -### 3\. 实操配置指南 (关键点) - -既然你只有两台 H20 并且要上 K3s,以下是落地的关键配置: - -#### 第一步:在宿主机启动 MinIO 和 Redis (后端) - -不要用 Helm,直接写一个 `docker-compose.yml` 放在 `/opt/ai-infra/` 下运行: - -```yaml -version: '3' -services: - minio: - image: minio/minio - network_mode: "host" # 直接用宿主机网络,性能最好 - volumes: - - /mnt/data1/minio_data:/data # 挂载到大容量HDD或SATA SSD - command: server /data --console-address ":9001" - - redis: - image: redis:7 - network_mode: "host" - volumes: - - /mnt/data1/redis_data:/data - command: redis-server --appendonly yes -``` - -#### 第二步:在 K3s 中安装 JuiceFS CSI Driver (前端) - -1. **安装 CSI Driver**: - - ```bash - helm repo add juicefs https://juicedata.github.io/charts/ - helm install juicefs-csi-driver juicefs/juicefs-csi-driver -n kube-system - ``` - -2. **创建 Secret (连接信息)**: - - ```yaml - apiVersion: v1 - kind: Secret - metadata: - name: juicefs-sc-secret - namespace: default - type: Opaque - stringData: - name: "myjfs" - metaurl: "redis://<宿主机IP>:6379/1" # 注意这里填宿主机内网IP - storage: "minio" - bucket: "http://<宿主机IP>:9000/mybucket" - access-key: "minioadmin" - secret-key: "minioadmin" - ``` - -3. **创建 StorageClass (核心中的核心)**: - 这里要配置**本地缓存**,让 JuiceFS 能够利用 H20 的 NVMe。 - - ```yaml - apiVersion: storage.k8s.io/v1 - kind: StorageClass - metadata: - name: juicefs-sc - provisioner: csi.juicedata.com - parameters: - csi.storage.k8s.io/node-publish-secret-name: juicefs-sc-secret - csi.storage.k8s.io/node-publish-secret-namespace: default - csi.storage.k8s.io/provisioner-secret-name: juicefs-sc-secret - csi.storage.k8s.io/provisioner-secret-namespace: default - - # === 关键性能参数 === - cache-dir: /mnt/nvme0/juicefs-cache # H20 宿主机上的 NVMe 路径 - cache-size: "2048000" # 2TB 缓存上限 - buffer-size: "512" # 写缓冲 - prefetch: "2" # 预读并发度 - ``` - -### 4\. 备选方案:如果 CSI 搞不定怎么办? - -如果 M1 阶段你发现 CSI Driver 调试太麻烦(有时候 K3s 的路径和标准 K8s 不一样,导致 CSI 挂载失败),可以退回到 **“Host Mount + HostPath”** 模式: - -1. **Outside**: 在宿主机直接运行 `juicefs mount ... /mnt/ai-data`。 -2. **Inside**: 在 Pod YAML 里直接映射: - ```yaml - volumes: - - name: data - hostPath: - path: /mnt/ai-data - type: Directory - ``` - -**结论:** -优先尝试 **CSI Driver** 方案,这才是正规军的做法,能为你的“平台化”愿景打下基础。MinIO/Redis 请务必保持独立,不要让 K3s 的波动影响到底层数据安全。 - - ----------------- - -# 关于Minio扩容 -这是一个非常关键的运维问题。 - -首先要泼一盆冷水(也是为了避坑):**MinIO 不支持从“单机模式(Standalone)”无缝平滑扩容到“分布式模式(Distributed)”**。 - - * **单机模式**下,MinIO 是直接把文件存成普通文件。 - * **分布式模式**下,MinIO 会启用**纠删码(Erasure Coding)**,把文件切片打散存储。 - -底层数据结构完全不同,因此你**不能**简单地把新节点加入旧集群。你必须通过\*\*“建立新集群 -\> 数据迁移 -\> 切换流量”\*\*的方式来完成。 - -鉴于你使用了 JuiceFS,这个迁移过程其实比纯 MinIO 迁移要灵活得多。以下是针对你 H20/K3s 环境的最佳实战方案: - ------ - -### 方案概览:蓝绿部署迁移法 - -我们不修改旧的 MinIO,而是搭建一个新的 3 节点集群,同步数据,然后改一下 K8s Secret 指向新集群。 - -#### 前置条件 - -假设你现有的环境是: - - * **Old MinIO**: 跑在 Node A (192.168.1.10) - * **New Target**: 准备在 Node A, Node B, Node C 上跑 3 节点分布式 MinIO。 - ------ - -### 第一步:搭建全新的 3 节点 MinIO 集群 - -由于旧的 MinIO 还在通过 `host` 网络运行(占用 9000 端口),新集群如果不关掉旧的,需要用**不同端口**(比如 9100)或者部署在不同机器上。 - -假设你新增了机器,或者错开端口。以下是 3 节点分布式 MinIO 的 `docker-compose.yml` 示例(需要在三台机器上都运行): - -```yaml -version: '3' -services: - minio-distributed: - image: minio/minio - network_mode: "host" - hostname: "node1" # 另外两台改为 node2, node3 - # 关键:分布式启动命令,必须列出所有节点 - command: server http://192.168.1.10:9100/data http://192.168.1.11:9100/data http://192.168.1.12:9100/data --console-address ":9101" - volumes: - - /mnt/data_new:/data # 挂载新的数据盘(或者旧盘的新目录) - environment: - MINIO_ROOT_USER: "admin" - MINIO_ROOT_PASSWORD: "strongpassword" -``` - -*注意:3 节点 MinIO 允许挂掉 1 台机器而不丢失数据。* - ------ - -### 第二步:数据迁移 (两种路径) - -鉴于你用的是 JuiceFS,这里有**两条路**可选: - -#### 路径 A:底座迁移(推荐,速度快,原汁原味) - -直接搬运 MinIO 里的对象块(Block)。因为 JuiceFS 把数据切成了固定的 Block 存在 MinIO 里,我们只需要把这些 Block 从旧 MinIO 搬到新 MinIO,**不需要经过 JuiceFS 客户端**。 - -1. **安装 `mc` (MinIO Client)** 命令行工具。 -2. **配置别名**: - ```bash - mc alias set oldm http://192.168.1.10:9000 minioadmin minioadmin - mc alias set newm http://192.168.1.10:9100 admin strongpassword - ``` -3. **全量镜像 (Mirror)**: - ```bash - # 创建新桶 - mc mb newm/mybucket - - # 开始同步数据 (将旧桶数据镜像到新桶) - # --watch 参数可以持续监听增量数据,适合不停机迁移 - mc mirror --watch oldm/mybucket newm/mybucket - ``` - -#### 路径 B:JuiceFS 层面迁移(适合要换云厂商/存储类型) - -如果你想顺便整理数据碎片,或者从 MinIO 迁移到 阿里云 OSS,可以用这个。但在你的场景下,路径 A 更快。 - - * 命令:`juicefs sync minio://... minio://...` (不推荐,因为需要解密再加密,消耗 CPU)。 - ------ - -### 第三步:停机切换 (Cutover) - -为了保证 100% 数据一致性,建议申请一个短时间的维护窗口(10-20分钟)。 - -1. **停止训练任务**:Scale down 所有的 Training Job。 -2. **停止旧 MinIO 写入**: - * 确保 `mc mirror` 已经追平了数据(没有 pending)。 - * 你可以把旧 MinIO 设为只读,或者直接停止旧容器。 -3. **最后一次 Sync**: - * 运行 `mc mirror --overwrite` 确保最后一点差异也同步了。 -4. **修改 K8s Secret**: - * 这是 JuiceFS 的魔力所在。你**不需要**重新格式化 JuiceFS,也**不需要**修改元数据引擎(Redis)。你只需要告诉 JuiceFS:“底下的对象存储换地方了”。 - * 编辑 K8s 里的 Secret: - - ```bash - kubectl edit secret juicefs-sc-secret - ``` - * 修改 `bucket` 和 `access-key/secret-key`: - - ```yaml - stringData: - # Redis 配置保持不变!元数据还在那里 - metaurl: "redis://192.168.1.10:6379/1" - - # 修改 bucket 地址为新集群地址 - # 注意:MinIO 分布式集群通常前面会加一个 Nginx 做负载均衡 - # 或者你可以填任意一个节点的 IP (MinIO 会自动转发) - bucket: "http://192.168.1.10:9100/mybucket" - access-key: "admin" - secret-key: "strongpassword" - ``` -5. **重启 CSI Driver**: - * 删除 JuiceFS CSI 的 Pod,让它们重建并加载新的 Secret。 - * 删除使用该 PVC 的 Pod。 - ------ - -### 第四步:验证与回收 - -1. **验证**: - * 启动一个测试 Pod,挂载 PVC。 - * `ls` 一下目录,确认文件都在。 - * `cat` 一个文件,确认能读(JuiceFS 会去新 MinIO 拿数据块)。 -2. **回收**: - * 确认一切正常运行几天后。 - * 下线旧的单节点 MinIO 容器。 - * 释放旧的磁盘空间。 - -### 专家建议 (Pro Tips) - -1. **负载均衡 (Load Balancer)**: - - * 到了 3 节点分布式阶段,建议在 3 台 MinIO 前面架设一个 **Nginx** 或者 **HAProxy**。 - * 这样 JuiceFS Client 连接的是 `http://nginx:9000`,Nginx 轮询转发给后端 3 台 MinIO。 - * **好处**:如果某台 MinIO 挂了,Nginx 自动剔除,训练任务完全无感。如果直连某台 IP,那台挂了训练就断了。 - -2. **元数据备份**: - - * 在做这一切之前,**务必备份 Redis 的 dump.rdb**。 - * 只要 Redis 在,JuiceFS 的文件结构就在。对象存储里的数据块丢了还能通过文件名找回来(虽然内容坏了),但 Redis 丢了,对象存储里那一堆 `chunk-xxx` 的文件就是一堆毫无意义的二进制垃圾,神仙难救。 - -3. **拓扑限制**: - - * MinIO 扩容通常是“倍增”或者“对等扩容”。比如 4 节点扩容,通常是再加 4 节点(变成 2 个 Server Pool)。 - * 所以,规划 3 节点时,最好磁盘大小一致,网络环境一致。 \ No newline at end of file diff --git a/specs/mvp/sw_arch.excalidraw b/doc/sw_arch.excalidraw similarity index 100% rename from specs/mvp/sw_arch.excalidraw rename to doc/sw_arch.excalidraw diff --git a/specs/mvp/image/Snipaste_2025-12-30_16-06-37.png b/specs/mvp/image/Snipaste_2025-12-30_16-06-37.png deleted file mode 100644 index fb62750..0000000 Binary files a/specs/mvp/image/Snipaste_2025-12-30_16-06-37.png and /dev/null differ diff --git a/specs/mvp/image/Snipaste_2025-12-30_17-02-20.png b/specs/mvp/image/Snipaste_2025-12-30_17-02-20.png deleted file mode 100644 index 4005c93..0000000 Binary files a/specs/mvp/image/Snipaste_2025-12-30_17-02-20.png and /dev/null differ diff --git a/specs/mvp/image/roadmap_v2.5.png b/specs/mvp/image/roadmap_v2.5.png deleted file mode 100644 index 6225039..0000000 Binary files a/specs/mvp/image/roadmap_v2.5.png and /dev/null differ diff --git a/specs/mvp/image/roadmap_v3.0.png b/specs/mvp/image/roadmap_v3.0.png deleted file mode 100644 index 8424345..0000000 Binary files a/specs/mvp/image/roadmap_v3.0.png and /dev/null differ diff --git a/specs/mvp/milestones.md b/specs/mvp/milestones.md deleted file mode 100644 index c983f1d..0000000 --- a/specs/mvp/milestones.md +++ /dev/null @@ -1,34 +0,0 @@ - -# milestones - -通过以下几个里程碑来梳理和分析确认可行性,最终目标是产出一套基于Native Ray集群(无k8s底座)的verl 训练平台,支持多用户,运行各类verl任务,提高整体集群的资源利用效率,并且能够通过监测系统进行观察和资源统计,监控报警。未来形成运维SOP后,接入运维智能体,执行自动化运维。 -- Workload - - ppo on ray - - grpo on ray - - sft on ray 可行性 - - model serving on ray - - customize code 自定义代码,任意verl example 提交代码 - - 自定义reward function - - 同时多verl版本支持,同时跑不同的ray任务,但是使用不同版本的verl,甚至是用户魔改版本 -- Ray Job管理 - - 通过python api提交,而不是通过ray cli提交 - - 任务排队机制。无优先级,多个pending job谁先满足资源就谁先执行。 - - 【确认支持】gang scheduling (all or nothing), 指定好trainer.nnodes和trainer.n_gpus_per_node参数,不满足就pending。 - - 无配额管理、公平调度等特性。 - - Ray本身不支持任务超时参数,需要单独job监控,发现超时才停止。 - - Pipeline管理【高级, 暂不实现】 - - 提供对Ray Job进一步封装,串联多个Ray Job,自动完成训练,模型合并等job串联 -- 可观测性 Observability - - 测试本地部署 weight and bias server 可行性,如何集成现有job流程 - - 测试部署 prometheus & grafana,对ray节点进行监测 - - job监控,哪些job使用了多少资源,跑了多长时间,资源利用率是否充分,是否空占着GPU -- 数据、模型存储管理 - - shared dataset管理:所有用户共享的hf数据集 - - hf 模型管理:所有用户共享的hf 基座模型库 - - user dataset 管理: 用户独自的数据集管理 - - user 模型管理:用户独自的模型管理,保存训练好的模型 - - job 作业数据管理,作业产出物,临时目录数据 - - user management:用户可以通过统一界面来管理自己是user dataset/model space和自己运行的job的临时目录,从而灵活组织任务流水线,提供灵活的文件查看方式 -- 网络 - - 确认是否支持IB(H100环境),以及RoCEv2(H20环境),需要怎样配置 - diff --git a/specs/mvp/mvp_roadmap.md b/specs/mvp/mvp_roadmap.md deleted file mode 100644 index 716d002..0000000 --- a/specs/mvp/mvp_roadmap.md +++ /dev/null @@ -1,348 +0,0 @@ -# MVP Roadmap(V1 → V2 → … → 训练平台) - -本文档在 `specs/mvp/milestones.md` 的草稿基础上做**扩展与细化**:把目标拆成可迭代的版本(MVP v1/v2/…),保证每个版本都能**独立运行、可验证验收**,并且在上一版本基础上演进。 - -> 总目标(North Star):产出一套**基于 Native Ray 集群(无 K8s 底座)**的训练平台,面向多用户,支持 `verl` 各类训练/评测/Serving 工作负载,提升集群利用率,并通过可观测系统实现资源统计、监控告警,最终形成运维 SOP 并可接入运维智能体做自动化运维。 - ---- - -## 0. 关键原则(贯穿所有版本) - -1) **版本可独立运行**:每个版本都能从“空环境”按文档跑起来(不依赖未来能力)。 -2) **验收可客观验证**:每个里程碑必须有明确的 DoD(Definition of Done)与可复现步骤。 -3) **强制产物落盘**:模型/数据/日志/ckpt 必须可追踪、可复用、可审计(基于共享存储/NFS)。 -4) **Head 不参与计算**:Head 只承担控制面(GCS/Dashboard/Job server),避免训练抢占控制面资源。 -5) **按 submission id 组织作业**:作业输出目录与 Ray submission id 绑定,方便检索、回收、归档。 -6) **“先把 RL 跑稳”,再扩 workload**:先 PPO(已验证),再 GRPO/SFT/Serving。 - ---- - -## 0.1 里程碑总览(建议交付顺序) - -| 版本 | 定位 | 关键交付 | 核心验收点 | -|---|---|---|---| -| v1 | 可复现实验闭环 | Ray 集群 + PPO 跑通 + 持久化 | driver 不在 head;产物落盘 | -| v1.1 | 实验工程化 | JobSpec 模板 + 新增 1 个 workload | 可回归、可定位、可扩展 | -| v2.0 | 服务化入口 | API + Ray Jobs SDK | API 提交/查询/停止可用 | -| v2.1 | 节点纳管 | SSH 注入 + 资源池/标签 | 节点上线/下线、gang 约束 | -| v3.0 | 平台雏形 | 队列 + 超时 + 最小多用户 | pending→running 自动调度 | -| v3.1 | 可扩展平台 | 自定义代码/reward + 多版本 | 多版本并存、插件可用 | -| v4.0 | 可运营平台 | Prom/Grafana + W&B | 资源核算/告警/归档 | -| v4.1 | 可交接平台 | SOP + 自动化运维接口 | 非开发可按 SOP 运维 | -| v5.0 | 长期形态 | Serving + Pipeline | 训练→发布推理闭环 | - -## 1. 当前基线:MVP v1(已完成/已验证) - -### 1.1 目标 - -在单机(或同一宿主机)用 3 个容器跑通: - -- Ray head(无 GPU,CPU=0/GPU=0) -- 2 个 Ray worker(每个 4 GPU) -- 通过 **head 上的 `ray job submit`** 提交 `verl` PPO(`total_epochs=1`) -- 通过 **entrypoint 自定义资源**强制 driver 在 worker 上 -- 数据/模型/日志/ckpt 全部持久化 - -### 1.2 交付物(repo 中已存在) - -- 脚本与 compose:`src/mvp/v1/` -- 行动与验收文档:`specs/mvp/v1/v1_action.md` -- 共享目录约定:`shared/datasets`、`shared/hf`、`shared/jobs` 等(与 NFS 对齐) - -### 1.3 验收口径(摘要) - -- `ray job list` 的 `driver_info.node_ip_address` ∈ worker IP,且 ≠ head IP -- 训练输出落在 `/mnt/shared/jobs//...` -- checkpoint 按 `save_freq` 产生(避免爆磁盘) - ---- - -## 2. MVP v1.1(Hardening + 多 workload 可行性验证) - -> 目标:把 v1 从“实验脚本”升级成“可长期回归的最小系统”,并验证更多 workload 的可行性边界。 - -### 2.1 主要能力 - -- Workload 扩展(可选顺序): - - PPO(回归金标) - - GRPO on Ray(可运行验证) - - SFT on Ray(可运行验证:`llamafactory` 或 `verl` 相关 SFT 路径) -- 作业模板化(最小实现): - - 统一 JobSpec(YAML/JSON)描述:workload 类型、资源(nnodes/n_gpus_per_node)、数据、模型、输出目录、超时 - - 仍然用 `ray job submit`,但把 entrypoint 组装逻辑标准化 -- checkpoint 策略与磁盘保护: - - 默认 `save_freq` ≥ 10(或按训练总 steps 的比例) - - 明确保留策略(至少提供“保留最后 N 个 ckpt”的配置建议/脚本) -- “失败可定位”: - - 统一收敛日志入口(Ray job logs + hydra 日志目录 + 关键参数快照) - - 失败时能定位:是资源不足 / NCCL / 数据 / 模型 / 配置错误 - -### 2.2 验收(DoD) - -- 同一套脚本在同一台机器能连续跑 3 次 PPO 回归,产物目录不互相覆盖 -- 至少新增 1 个 workload(GRPO 或 SFT)可以跑通 “启动→训练→落盘” 闭环 -- 作业目录内包含: - - `config/submit_cmd.txt`(或 job spec 快照) - - `logs/`(可追踪) - - `checkpoints/`(按策略生成) - ---- - -## 3. MVP v2.0(Control Plane 服务化:API + Ray Jobs SDK) - -> 目标:从“人跑脚本”升级为“服务提交任务”。依然是 Native Ray 集群,但引入一个最小控制平面服务。 - -### 3.1 系统形态 - -- Control Plane(建议部署在 head/CPU 机器): - - FastAPI 服务(REST) - - Job 管理:用 Ray Jobs **Python SDK** 提交/查询/停止(不再依赖 CLI 文本解析) - - 节点视图:读取 Ray state(nodes, actors, placement groups) -- Data Plane: - - 仍然是预先启动的 worker 节点加入集群(先不做 SSH 动态纳管也可) - -### 3.2 API(MVP 级别) - -- `POST /v1/jobs`:提交 JobSpec(ppo/grpo/sft) -- `GET /v1/jobs`:列表(含状态、资源、开始/结束时间) -- `GET /v1/jobs/{id}`:详情(含输出目录、driver node) -- `POST /v1/jobs/{id}:stop`:停止作业 - -### 3.3 验收(DoD) - -- API 提交 PPO,返回 submission id;输出目录为 `/mnt/shared/jobs//...` -- API 查询 job 状态与 driver node(必须是 worker) -- 停止 job 后,资源释放、状态可见 - ---- - -## 4. MVP v2.1(SSH 纳管 + 资源池 + Gang 约束) - -> 目标:对齐你草稿里“SSH 纳管”的约束与需求:控制面能纳管 GPU 节点,形成可运营的资源池。 - -### 4.1 节点纳管(SSH Provisioner) - -- 控制面保存 NodeSpec(ip/user/port/labels/gpu_count) -- 通过 SSH 执行: - - `ray start --address=:6379 --resources=...` - - `ray stop`(drain/下线) -- 维护节点状态机:`pending → online → draining → offline` - -### 4.2 资源池与 gang(All-or-nothing) - -- 资源池最小模型: - - pool 标签(如 `pool_a`、`h20`、`ib_domain_1`) - - 提交 job 时指定 pool 约束 -- Gang 约束(MVP 实现方式): - - job spec 明确 `trainer.nnodes` + `trainer.n_gpus_per_node` - - 提交前检查 Ray 可用资源是否满足,不满足则进入 pending 队列(见 v3.0) - -### 4.3 验收(DoD) - -- 通过 API 注册 2 个 worker(SSH 注入 ray start)后,`ray status` 可见节点上线 -- 通过 API 下线节点,节点被标记不可调度且不再分配新 job -- gang 不满足时 job 不提交(或提交后一直 pending),满足后可运行 - ---- - -## 5. MVP v3.0(调度与多用户:队列 + 超时 + 最小权限) - -> 目标:平台开始“像个平台”:多用户、队列、超时、审计。仍然不做复杂配额/公平调度。 - -### 5.1 作业队列(简单但可用) - -- FIFO 队列:无优先级 -- “资源满足就调度”:谁先满足谁先跑(可接受非严格 FIFO) -- job 超时:Ray 原生不支持统一 timeout(草稿已指出),因此控制面需: - - 记录 start_time - - 定期扫描超时 job → `stop` - -### 5.2 多用户最小闭环 - -- 认证(MVP):token 或 basic auth(先不做复杂 RBAC) -- 归属与隔离(文件层): - - `/mnt/shared/users//datasets/` - - `/mnt/shared/users//models/` - - `/mnt/shared/jobs//` 记录 user/metadata - -### 5.3 验收(DoD) - -- 2 个用户可各自提交 job,能看到自己的 job 列表与输出目录 -- 超时策略可触发(模拟短 timeout),job 被停止且状态标记为 timeout -- 队列在资源不足时保持 pending,资源释放后自动运行 - ---- - -## 6. MVP v3.1(可扩展性:自定义代码/Reward、多版本 VERL) - -> 目标:把“平台内置 workload”升级成“用户可提交自定义代码与 reward”,并支持多版本并存。 - -### 6.1 自定义代码提交(最小实现) - -两种方式二选一(建议先做 A): - -- A:`working_dir` 指向 NFS 上的代码快照目录(用户自己准备/上传) -- B:上传 zip(控制面落到 NFS 并解压为 code snapshot) - -### 6.2 多版本 VERL 并存 - -约束前提:**基础镜像保持同一个**(生产环境容器由算力平台创建时已固定镜像标签)。 - -目标:在同一 Ray 集群内,不同 job 可以使用不同版本的 `verl`(例如不同分支/commit 或用户魔改版)。 - -已确认优先方案(A):**必须通过 Ray Job 的 `runtime_env.env_vars` 透传 `PYTHONPATH`**,让 job 粒度优先 import 指定代码快照。 - -建议方案(以 NFS 为中心,最小可行实现): - -- 在共享存储上以“不可变快照”的方式存放代码版本(推荐 commit hash 命名): - - `${SHARED_ROOT}/common/code/verl//...` - - `${SHARED_ROOT}/users//code/verl//...`(用户魔改版) -- JobSpec 增加 `code_path`(指向上述目录),控制面在提交 job 时注入(必须走 runtime_env): - - `runtime_env.env_vars.PYTHONPATH = ":$PYTHONPATH"`(把 code_path 放最前面,确保 import 优先级) - -示例(概念性,实际以 `${SHARED_ROOT}` 为准): - -```bash -CODE_PATH="${SHARED_ROOT}/common/code/verl/" - -ray job submit \ - --address="http://127.0.0.1:8265" \ - --submission-id="" \ - --runtime-env-json='{"env_vars": {"PYTHONPATH": "'"${CODE_PATH}"':$PYTHONPATH"}}' \ - -- \ - python3 -m verl.trainer.main_ppo ... -``` - -需要验证的关键点(作为 v3.1 的 DoD 之一): - -- 同时运行两个 job: - - jobA 使用 ``,jobB 使用 `` - - 互不影响,且各自训练/日志/ckpt 正常 -- job 粒度是否能做到“依赖隔离”(至少做到 `verl` 版本隔离;第三方依赖冲突可先假设镜像内一致) - -> 备注:当前 v1 的做法是容器内全局 `pip install -e /workspace/verl`,这会让所有 job 默认使用同一份 `verl`。要实现多版本并存,必须让 job 的 import 优先使用 `code_path`(或为每个 job 单独创建 venv/安装 wheel;后者更重,建议后置)。 - -### 6.3 自定义 reward function - -- JobSpec 支持 `reward_fn_path`(Python 模块路径) -- `reward_fn_path` 可指向共享存储中用户自定义代码目录(例如 `${SHARED_ROOT}/users//code/...`) - - 约束:代码必须在 job runtime 中可 import(由 `working_dir`/`PYTHONPATH` 或 runtime_env 保障) -- 控制面校验模块可导入(basic lint/安全白名单可后置) - -### 6.4 验收(DoD) - -- 同时运行两个 job:使用不同的 `verl` 代码版本(或用户魔改版本),互不影响 -- 用户可在 JobSpec 中替换 reward function 并跑通一个最小训练闭环 - ---- - -## 7. MVP v4.0(可观测性:Prometheus/Grafana + W&B 集成) - -> 目标:平台可运营:能回答“谁在用多少资源、跑了多久、利用率如何、是否空占 GPU”。 - -### 7.1 指标与监控 - -- Ray 指标接入 Prometheus(节点/任务/actor) -- GPU 指标:nvidia exporter 或 DCGM exporter -- Dashboard:Grafana(至少 3 张核心面板) - - 集群总 GPU/CPU 使用率、空闲率 - - 每 job 的 GPU 时间、峰值显存、运行时长 - - 节点健康(心跳/掉线)与告警 - -### 7.2 W&B(或等价)集成验证 - -- 最小可行:单机 self-host W&B server 可用性验证 -- JobSpec 支持启用/关闭 W&B,并传入 project/run name - -### 7.3 验收(DoD) - -- Grafana 上能看到集群与 job 资源视图 -- 某个 job GPU 利用率异常(模拟)能触发告警规则(邮件/IM/日志即可) -- W&B 指标能按 job 维度归档(至少 PPO 能上报) - ---- - -## 8. MVP v4.1(运维化:SOP + 自动化运维接口) - -> 目标:把平台变成“可交接”的系统:运维动作标准化,并为智能体留出接口。 - -### 8.1 SOP 与自动化入口 - -- SOP 文档: - - 节点上线/下线 - - 故障定位(Ray session、Ray job、NCCL、OOM) - - 资源回收(停止 job、清理 ckpt) -- 自动化接口(最小): - - `/v1/ops/drain_node` - - `/v1/ops/restart_ray_head`(谨慎:需要保护与权限) - - `/v1/ops/cleanup_job_artifacts` - -### 8.2 验收(DoD) - -- 按 SOP,非开发人员可完成一次“节点上线→跑任务→下线→清理” -- 自动化接口至少能完成 1 个高频动作(如清理/停止/下线) - ---- - -## 9. MVP v5.0(Serving 与 Pipeline,偏长期) - -> 目标:训练-部署一体化:支持 model serving,并在平台内串联训练→评测→发布。 - -### 9.1 Serving - -- Ray Serve(或等价)部署模型推理服务 -- Serving 与训练共用模型库与权限(按 user/project) - -### 9.2 Pipeline(草稿里标为高级) - -- Pipeline 是对多个 job 的封装(训练→merge→eval→publish) -- 可先实现最小 DAG(两步串联)作为验证 - -### 9.3 验收(DoD) - -- 训练产物一键发布为一个可访问的推理 endpoint -- Pipeline 能自动串联并产出最终 artifact(可回滚/可追踪) - ---- - -## 10. 并行技术验证(建议尽早做) - -这些属于“跨版本”风险项,建议在 v1.1 ~ v2.0 期间尽早做: - -### 10.1 网络(IB / RoCEv2) - -- 确认环境是否支持 IB(H100)或 RoCEv2(H20) -- 跑最小 NCCL 通信验证(all-reduce / bandwidth) -- 将必要的 NCCL 环境变量注入到 job runtime_env - -### 10.2 Ray + 多节点容器约束 - -- 多容器同宿主机时的 Ray node_ip/临时目录冲突规律(已踩坑,需固化规范) -- 端口范围与防火墙策略(Ray worker 端口、dashboard、metrics) - ---- - -## 11. 已确认的约束与假设(来自讨论结论) - -这些会直接影响 v2.1(SSH 纳管)与后续多用户/存储设计: - -1) **最终形态仍以“每节点容器”运行**(不是裸机 systemd)。 - - H20 开发环境:我们可在宿主机用 `docker compose` 自建容器,并通过 SSH 进入容器调试/纳管。 - - H100 生产环境:容器由算力平台创建/回收;平台侧控制面只能 **SSH 进入这些容器** 做纳管(执行 `ray start/stop`、注入 env 等)。 -2) **认证**:内部 token 即可(MVP 阶段不对接 SSO)。 -3) **存储**:只考虑 NFS。 - - 开发环境:NFS/共享目录可通过宿主机 bind mount 提供给容器。 - - 生产环境:所有容器挂载相同 NFS,容器内共享根路径为 `/private/`(需要在实现时把“共享根路径”做成可配置项,而不是写死 `/mnt/shared`)。 -4) **网络拓扑约束**:暂不做按 IB 域/机架/拓扑的强约束调度(第 10.1 仍需验证 IB/RoCE 是否可用与配置方式,但调度不引入拓扑维度)。 -5) **共享目录分层**:在 `users//...` 之外增加一个可读写的 `common/` 目录用于共享数据/模型/代码: - - `${SHARED_ROOT}/common/datasets/` - - `${SHARED_ROOT}/common/models/` - - `${SHARED_ROOT}/common/code/` - - 权限(MVP):先默认“所有内部 token 用户可读写”,后续再细化只读/受控写。 - ---- - -## 12. 仍需你确认/讨论的问题(剩余不确定项) - -1) `runtime_env.env_vars` 注入对“子进程/训练框架内部启动进程”的覆盖范围是否足够? - - 需要确认 `verl`/`sglang` 等子进程是否继承 driver 的环境变量(通常会继承,但建议在 v3.1 验收时明确验证)。 diff --git a/specs/mvp/mvp_roadmap_v2.md b/specs/mvp/mvp_roadmap_v2.md deleted file mode 100644 index d6704d4..0000000 --- a/specs/mvp/mvp_roadmap_v2.md +++ /dev/null @@ -1,133 +0,0 @@ -这一版的设计采用了 **Overlay 架构 + GPFS 核心存储 + 无状态(Stateless)节点池** 的模式,逻辑非常自洽且具备极高的云原生弹性。 - ---- - -### **项目代号:AI Infra Overlay Platform (Stateless Ray + GPFS)** - -#### **阶段一:内核构建与验证 (Kernel & Verification)** - -*目标:验证核心计算逻辑,跑通“提交-执行”的最小闭环。* - -* **v1.1: 原型验证 (Verl Task Spec & Ray Job)** -* **核心功能**:实现基础的任务定义与提交。 -* **组件**: -* `Ray Job Tool (Ray Client)`:客户端工具。 -* `VerlTaskSpec YAML`:定义多代码路径 (Multi-Verl Code Path) 和任务参数。 - - -* **基础设施**:Handmade Ray Cluster(手工搭建的集群),用于验证核心代码。 - - -* **v2.0: 任务管理层 (Task Management)** -* **核心功能**:引入服务端,管理任务生命周期。 -* **新增组件**: -* `API Server`:统一接口层。 -* `Task Management`:实现任务的队列 (Queue)、映射 (Map) 和重试 (Resubmit) 机制。 - - -* **基础设施**:仍运行在手工集群上,但控制面开始服务化。 - - - ---- - -### **阶段二:架构质变 - 无状态节点池 (The Stateless Shift)** - -*目标:通过 GPFS 实现控制反转 (IoC),彻底解耦平台层与计算节点层。这是本架构最关键的转折点。* - -* **v2.5: 用户管理 & 无状态 Ray 节点池 (User Mgmt & Stateless Ray Node Pool)** * **核心机制:基于 GPFS 的服务发现 (Service Discovery)** -* **Ray Head (有状态)**:由 `Node Management` 启动(通常通过 SSH 或 K8s StatefulSet)。启动后,将自身的 IP 地址写入 GPFS 中的 `Head IP File`。 -* **Ray Worker (无状态)**: -* **Stateless**:Worker 容器启动时不依赖平台指令。 -* **Auto Connect**:启动脚本读取 GPFS 中的 `Head IP File`,获得 Head 地址并自动加入集群。 -* **Watchdog**:Worker 内部运行看门狗进程,监控 Head IP 变化。如果 Head 变动,Worker 自动重启或重连,实现自愈。 -* **新增组件**: -* `User Management`:多用户隔离。 -* `GPFS`:取代了之前的 JuiceFS,作为唯一的共享存储和元数据交换媒介。 - - - - - ---- - -### **阶段三:产品化与高级能力 (Productization & Advanced Features)** - -*目标:发布首个正式版本,并支持大模型训练所需的复杂网络与推理能力。* - -* **v3.0: 正式发布版 (Release v1.0)** * **里程碑**:**1st Version to Release!!** -* **核心功能**:闭环用户数据流。 -* **新增组件**: -* `WebUI`:可视化操作界面。 -* `Data Management (SFTPGo)`:用户上传数据/代码 -> SFTPGo -> 写入 GPFS -> Ray Worker 可见。 - - -* **基础设施**:全量切换到 `Ray Worker Node` (Stateless) + `GPFS` 的架构。 - - -* **v3.5: 高级定制与训推一体 (Advanced Task & Serving)** * **核心功能**:支持复杂的科研需求。 -* **新增组件**: -* `Model Serving`:支持模型推理服务。 -* `Advanced VerlTaskSpec`:支持自定义 Reward Function、自定义代码、Checkpoint 断点续训 (Resubmit from last checkpoint)。 - - -* **网络增强**: -* **IB Network Supporting**:支持 InfiniBand 网络,确保多机训练的高性能互联。 - - - - - ---- - -### **阶段四:全链路可观测性 (Full-Stack Observability)** - -*目标:打开黑盒,监控基础设施与业务指标。* - -* **v4.0: 系统级可观测性 (System Observability)** * **核心功能**:监控集群“活着”且“健康”。 -* **新增组件**: -* `Prometheus` + `Grafana` + `ELK`:指标与日志平台。 -* `Exporter`:部署在 Ray Worker Node 中的监控探针(采集 GPU/CPU/GPFS IO 指标)。 - - - - -* **v4.5: 算法级可观测性 (ML Observability)** * **核心功能**:监控模型“练得好不好”。 -* **新增组件**: -* `Weights & Bias (WanB)`:集成实验追踪工具,记录 Loss 曲线和训练参数。 - - - - - ---- - -### **阶段五:智能化运维 (AIOps)** - -*目标:迈向自动化与自治。* - -* **v5.0: 智能运维闭环 (Operability)** * **核心功能**:降低运维成本,提升稳定性。 -* **新增组件**: -* `Statistics`:集群资源利用率统计报表。 -* `SOP Tools`:标准运维工具(如自动清理 GPFS 垃圾文件、僵尸节点检测)。 -* `Agent`:智能运维助手(基于 LLM 的日志分析与故障诊断)。 - - - - - ---- - -### **新架构核心亮点总结** - -1. **极简的节点管理**: -* 利用 v2.5 的 **Head IP File + Watchdog** 机制,平台层不再需要维护复杂的 Worker IP 列表和 SSH 连接池。 -* **扩缩容极其简单**:只需在底层(K8s/Docker)增加 Worker 副本数,它们就会自动通过 GPFS 找到 Head 并加入战斗。 - - -2. **统一的数据平面 (GPFS)**: -* 从 v2.5 开始,GPFS 承担了 **数据存储** (Code/Data)、**状态同步** (Head IP) 和 **检查点存储** (Checkpoints) 三大职责,架构非常收敛。 - - -3. **高弹性 (Resilience)**: -* Worker 的 **Watchdog** 机制确保了当 Head 重启或网络抖动时,集群具备自我修复能力,无需人工干预。 \ No newline at end of file diff --git a/specs/mvp/refactor/code_refactor.md b/specs/mvp/refactor/code_refactor.md deleted file mode 100644 index 58ea4f6..0000000 --- a/specs/mvp/refactor/code_refactor.md +++ /dev/null @@ -1,301 +0,0 @@ -# MVP 代码结构重构方案(按功能模块划分) - -背景:当前 `src/mvp/` 下以 `v1.1/`、`v2.0/` 版本目录来组织代码。实际上 **v2.0 是在 v1.1 的 Ray Jobs SDK 提交链路基础上扩展了服务层**,并且为了让 v2.0 工作又对 v1.1 的 `docker-compose.yaml`、`dev.yaml` 做了修改(挂载 v2、开放 8080、增加 `v2:` 配置段)。因此“按版本分目录”会让依赖关系变得不清晰(谁是库、谁是应用、哪些配置是共享的)。 - -本方案目标:把 `src/mvp/` 重构为“按功能模块”划分(ray 提交核心库 / service 服务层 / cli 工具 / TaskSpecs / configs / scripts),并给出迁移后的验证与执行方案。 - -> 本文仅给出设计与迁移/验证方案,不直接改代码(待确认后再实施)。 - ---- - -## 1. 现状梳理(问题点) - -### 1.1 代码重复与耦合 - -- `src/mvp/v2.0/py/mvp_v11/` 是从 `src/mvp/v1.1/py/mvp_v11/` 复制而来用于复用,但这导致: - - **库代码重复**(修 bug 要改两份) - - 谁是“权威实现”不明确 -- v2 API(`mvp_v2`)通过引用复制的 `mvp_v11.RayJobTool` 来提交 Ray Job,本质上依赖 v1.1 提交链路作为“库”。 - -### 1.2 配置与部署目录不稳定 - -- v2.0 复用了 v1.1 config 文件并新增 `v2:` section,这是合理的“向后兼容扩展”,但它把: - - “Ray submit 基础配置” - - “API 服务配置” - - “部署路径约定(/workspace/mvp/v1.1 vs /workspace/mvp/v2)” - 混在一个文件里,不利于长期维护。 - -### 1.3 命名歧义:jobspec 与 Ray job - -- v1.1/v2.0 都使用 `jobspec.yaml` 指代“训练语义参数”(PPO/GRPO/SFT 的训练字段)。 -- 但 Ray 也有 “Ray Job” 概念(submission_id、entrypoint、runtime_env 等),易造成沟通误解。 -- 需要把训练侧 specs 改名为 **TaskSpecs**(代表平台级任务规范),与 Ray Job 区分。 - ---- - -## 2. 重构目标(What good looks like) - -### 2.1 目录与职责清晰 - -- “提交 Ray Job 的 SDK 封装”是一个可复用模块(库)。 -- “服务层(API + scheduler + SQLite)”是一个独立模块(应用/服务)。 -- “训练语义参数(TaskSpecs)”与 “Ray Job 提交参数(RayConfig)”分层清楚。 - -### 2.2 单一真源(Single Source of Truth) - -- 只能有一份“Ray submitter core”实现(不能复制一份到另一个版本目录)。 -- API 与 CLI/脚本都复用同一份 core。 - -### 2.3 兼容现有运行方式(渐进迁移) - -- 保留现有的脚本式启动/准备流程(Ray 起集群、准备模型/数据仍用 scripts)。 -- 允许在迁移期提供薄 wrapper 兼容旧路径(减少一次性 break)。 - ---- - -## 3. 目标结构(按功能模块划分) - -建议把 `src/mvp/` 重构为下面的“功能分层”: - -``` -src/mvp/ - py/ - argus/ # 顶层包(避免与 Ray 的 `ray` 包冲突) - __init__.py - - core/ # 通用:yaml/模型定义/工具函数(纯库) - __init__.py - yaml_io.py - ids.py # task_id / attempt_id 生成规则 - - ray/ # Ray Job 提交“核心库”(由现成 mvp_v11 迁移而来) - __init__.py - models.py # RayConfig, TaskSpec(解析), Attempt, enums - builders.py # build_training_argv (ppo/grpo/sft) - driver_entrypoint.py # 仍然作为 Ray job entrypoint(worker 上执行) - ray_job_tool.py # Ray Jobs SDK 封装(submit/status/stop/logs) - runtime_env.py # 统一 PYTHONPATH/runtime_env 组装逻辑 - - service/ # 服务层:FastAPI + scheduler + sqlite(应用) - __init__.py - app.py - scheduler.py - db.py - config.py # service 相关配置读取(从 configs 读取) - ray_resources.py - - cli/ # 命令行/SDK 提交入口(由现成 v1.1 run.py 迁移而来) - __init__.py - run.py # submit/status/logs/stop 等 action - - server.py # uvicorn 入口(导入 argus.service.*) - - configs/ - dev.yaml # RayConfig + ServiceConfig(按层次组织、可扩展) - prod.yaml # (可选)生产配置模板 - - taskspecs/ # 原 jobspecs/,改名 TaskSpecs(训练语义规范) - ppo.yaml - grpo.yaml - sft.yaml - README.md # TaskSpec 字段解释、示例 - - scripts/ # 宿主机脚本(docker exec/compose 编排) - lib.sh - 00_prereq_check.sh - 01_up.sh / 02_down.sh - 20_start_head.sh / 21_start_workers.sh - 30_prepare_data_and_model.sh - 40_submit_cli.sh # 通过 cli/run.py 提交 TaskSpec - 60_start_api.sh # 启动 API(service) - 61_stop_api.sh - 62_status_api.sh - - docker-compose.yaml # dev 环境 compose(从 v1.1 迁移到这里,路径稳定) - README.md # 总入口文档(运行方式、目录约定) -``` - -### 3.1 关键点:库 vs 应用边界 - -- `argus.ray` 是唯一的 Ray submitter 库(替代当前 v1.1/v2.0 的 `mvp_v11` 双份拷贝)。 -- `argus.service` 依赖 `argus.ray`,不反向依赖。 -- `argus.cli` 依赖 `argus.ray`,用于脚本化提交/调试。 - -### 3.2 TaskSpecs vs RayConfig - -- `taskspecs/*.yaml`:描述训练任务语义参数(workload、nnodes、n_gpus_per_node、数据/模型路径、训练步数等)。 -- `configs/*.yaml`:描述 Ray 提交环境(address、entrypoint_resources、runtime_env 以及 service 配置)。 - ---- - -## 4. 配置策略(重构后如何组织 configs) - -### 4.1 建议的 config 分层 - -把当前 `dev.yaml` 的内容明确分为两段(按模块名分段): - -1) `ray:`(RayConfig) -- job server address -- shared_root(`/private`) -- entrypoint resources(强制 driver 落 worker) -- runtime_env env_vars(HF cache、PYTHONPATH 注入策略) - -2) `service:`(ServiceConfig) -- api host/port -- auth token_env -- sqlite db_path -- scheduler tick/max_running/retry_interval - -示例(结构示意): - -```yaml -ray: - address: "http://127.0.0.1:8265" - shared_root: "/private" - entrypoint_num_cpus: 1 - entrypoint_resources: - worker_node: 1 - runtime_env: - env_vars: - HF_ENDPOINT: "https://hf-mirror.com" - PYTHONUNBUFFERED: "1" - user_code_path: "/private/user/code" - -service: - api: - host: "0.0.0.0" - port: 8080 - auth: - token_env: "MVP_INTERNAL_TOKEN" - sqlite: - db_path: "/private/common/db/mvp.sqlite3" - scheduler: - tick_s: 5 - retry_interval_s: 60 - max_running_tasks: 1 -``` - -> 迁移期可以支持“旧格式”(v1.1 顶层字段 + v2: 段)与“新格式”(ray:/service: 两段)并存:解析时兼容读取,降低一次性改动风险;但最终以新格式为准。 - ---- - -## 5. 迁移路径(推荐分两阶段实施) - -### 阶段 A:先拷贝/迁移现成文件,再做最小调整(保持行为不变) - -目标:不改功能、不改 API 行为。优先通过“拷贝/迁移现成文件 + 修正包引用/路径”完成重构,避免重头重写逻辑(降低出错概率)。 - -建议步骤: - -1) 抽出 `src/mvp/py/argus/ray/`(由现成代码迁移) - - 将 `src/mvp/v1.1/py/mvp_v11/` 迁移到 `src/mvp/py/argus/ray/`,并把它作为 submitter core 的唯一真源(不再保留一份复制品在其它目录)。 - - 只做机械化调整:修正 import、修正默认路径常量(例如 tool code path / working dir)、修正 scripts 的调用路径。 - -2) 抽出 `src/mvp/py/argus/service/`(由现成代码迁移) - - 将 `src/mvp/v2.0/py/mvp_v2/` 迁移到 `src/mvp/py/argus/service/`。 - - service 侧对 submitter 的依赖统一改为 `src/mvp/py/argus/ray/`(不再引用 `src/mvp/v2.0/py/mvp_v11/` 的复制品)。 - -3) CLI 统一入口:`src/mvp/py/argus/cli/run.py`(由现成代码迁移) - - 将 `src/mvp/v1.1/py/run.py` 迁移到 `src/mvp/py/argus/cli/run.py`,保留 action 语义(submit/status/logs/stop)。 - - 仅调整 import 与默认路径,使其指向新目录(configs/taskspecs/py)。 - -4) scripts 合并(以 v1.1 为基、合入 v2 API) - - 将 `src/mvp/v1.1/scripts/` 迁移到 `src/mvp/scripts/`(Ray 集群编排最成熟)。 - - 将 `src/mvp/v2.0/scripts/` 的 API 启停脚本合入 `src/mvp/scripts/`,并统一命名与默认路径。 - -5) docker-compose / mounts 稳定化(你已确认要迁移) - - 将 `src/mvp/v1.1/docker-compose.yaml` 迁移为 `src/mvp/docker-compose.yaml`。 - - 容器内挂载统一:宿主机 `.../src/mvp/` → 容器 `/workspace/mvp/`(包含 `py/ configs/ taskspecs/ scripts/`)。 - - runtime_env 的 `PYTHONPATH` 注入统一指向 `/workspace/mvp/py`(不再出现 `/workspace/mvp/v1.1/py`、`/workspace/mvp/v2/...`)。 - -阶段 A 完成标准: -- 原来 v1.1 的 CLI 提交方式仍可用(提交 PPO/GRPO/SFT)。 -- v2 API 仍可用(队列、取消、日志)。 -- 不再存在 `mvp_v11` 的重复目录。 - -### 阶段 B:配置格式升级(按模块两段)+ TaskSpecs 更名落地 - -目标:把 jobspec 真正改名为 TaskSpec,并把 config 升级为按模块两段(`ray:`/`service:`)清晰分层。 - -建议步骤: -- `jobspecs/` → `taskspecs/`,并更新 README/脚本引用。 -- `dev.yaml` 从“顶层字段 + v2:”迁移为“ray:/service:”两段。 -- 保留一段时间的兼容解析逻辑(读取旧格式时发出 warning)。 -- 完成验证后删除旧版本目录:`src/mvp/v1.1/`、`src/mvp/v2.0/`(以及远端对应目录),确保新结构成为唯一入口。 - -阶段 B 完成标准: -- docs 里不再出现 “jobspec” 词汇,统一称 “TaskSpec”。 -- `configs/dev.yaml` 分层清晰(`ray:`/`service:` 两段按模块名),服务与 Ray 的配置互不干扰。 - ---- - -## 6. 重构后的验证与执行方案(验收/回归) - -### 6.1 本地(仓库内)静态验证 - -1) import / 入口检查(在容器环境) -- `python3 -c "from argus.ray.ray_job_tool import RayJobTool"` -- `python3 -c "from argus.service.app import create_app"` - -2) 目录结构检查 -- 确保 `src/mvp/py` 是唯一 python 代码根 -- 确保 `taskspecs/`、`configs/`、`scripts/` 都在 `src/mvp/` 下 - -### 6.2 dev 环境(argus@h1)端到端验证 - -前置: -- 远端目录:`argus@h1:/home2/argus/infra/mvp/`(维持不变) -- 共享目录:`/home2/argus/infra/mvp/shared` 挂载到容器 `/private` - -验证步骤(推荐顺序): - -1) 重新拉起容器 + 启动 Ray -- `scripts/01_up.sh` -- `scripts/20_start_head.sh`(head `--num-cpus=0 --num-gpus=0`) -- `scripts/21_start_workers.sh`(workers 带 `worker_node` 资源) -- `ray status` / `ray list nodes` 确认 head 无 GPU、workers 各 4 GPU - -2) CLI 提交回归(等价 v1.1) -- 提交 `taskspecs/sft.yaml`,确认成功 -- 提交 `taskspecs/grpo.yaml`,确认成功 -- 提交 `taskspecs/ppo.yaml`,确认成功 - -3) API 服务回归(等价 v2.0) -- `scripts/60_start_api.sh` -- `POST /api/v2/tasks`(raw YAML TaskSpec) -- `GET /api/v2/tasks/{task_id}`:确认返回 `created_at/updated_at` -- `POST /api/v2/tasks/{task_id}:cancel`:确认任务 `state=CANCELED` 且 attempt `ray_status=STOPPED`(服务侧语义一致) - -4) 队列行为回归 -- 在 `service.scheduler.max_running_tasks=1` 下: - - 连续提交两个 8-GPU 任务:第二个应保持 `QUEUED/PENDING_RESOURCES`,直到第一个结束后自动提交。 - -验收标准: -- 三种 workload(PPO/GRPO/SFT)都能通过 CLI 跑通(或至少能正确提交到 Ray 并进入 RUNNING)。 -- API 提交/查询/取消/日志正常。 -- “cancel 后 state=CANCELED 但 attempt 仍 RUNNING”的不一致问题不再出现。 - ---- - -## 7. 风险与注意事项 - -1) PYTHONPATH 注入路径变化 -- 当前 runtime_env 里有 `MVP_TOOL_CODE_PATH=/workspace/mvp/v1.1/py` 的历史路径假设; -- 重构后需统一为 `/workspace/mvp/py`,并确保所有容器都挂载到相同路径。 - -2) SQLite WAL 在 NFS 上的稳定性 -- 目前 db 使用 WAL(生成 `-wal/-shm`),NFS 下可能有一致性风险; -- 可作为后续优化:检测 NFS 时退回 `journal_mode=DELETE` 或换成单机本地盘。 - -3) 渐进迁移的兼容期 -- 迁移期可以短暂保留旧路径(例如 `src/mvp/v1.1`、`src/mvp/v2.0` 仅保留 README 指向新路径)以减少一次性断裂;但你已确认最终会删除这两个目录,因此需要在 scripts/文档同步更新后尽快清理。 - ---- - -## 8. 已确认约束(将按此实施) - -你已明确: - -1) `docker-compose.yaml` 必须迁移到 `src/mvp/` 根;重构完成后 `src/mvp/v1.1`、`src/mvp/v2.0` 都会删除。 -2) `configs/dev.yaml` 升级为两段,按模块名分段:`ray:` 与 `service:`;并保持纯 YAML 风格(不混用 JSON inline map)。 -3) TaskSpec 字段先保持与现有 v1.1 YAML 完全一致(仅目录与命名变化),避免引入不必要的不兼容。 diff --git a/specs/mvp/remain_problems.md b/specs/mvp/remain_problems.md deleted file mode 100644 index 44510aa..0000000 --- a/specs/mvp/remain_problems.md +++ /dev/null @@ -1,27 +0,0 @@ - -# v3.6 -wandb 映射目录是/vol 固定问题: -查过官方文档/公开资料后结论是:wandb/local(W&B local server 容器)没有提供“把服务端持 - 久化根目录从 /vol 改成别的路径”的官方环境变量/启动参数。官方用法一直是假设你把持久化卷 - 挂到容器内的固定路径 /vol(例如 -v :/vol)。(github.com (https://github.com/ - wandb/server)) - - 需要注意区分两类“目录”: - - - 服务端(wandb/local 容器):持久化目录是容器内固定 /vol,用于保存实例元数据、账号/初 - 始化信息等(license 也可以用 env 配,但数据目录仍是 /vol)。(github.com (https:// - github.com/wandb/server)) - - 训练侧(wandb Python SDK / VERL 任务):WANDB_DIR、WANDB_DATA_DIR 等环境变量只影响“客 - 户端本地生成文件/缓存”,不改变服务端容器的数据落盘路径。(docs.wandb.ai (https:// - docs.wandb.ai/platform/hosting/env-vars)) - - 所以如果你现在的约束是“只能挂 ../../shared:/private,不能再额外挂 ../../shared/common/ - wandb:/vol”,要把 W&B 服务端数据落到 shared 下面,现实可行的路子是: - - - 自定义 W&B 容器 entrypoint(或 wrapper)在启动前做一次 ln -s /private/common/wandb / - vol(或 bind-mount 到 /vol),让服务仍然写 /vol,但实际落到 /private/common/wandb。 - 这属于“容器层改造”,不是 W&B 官方参数。 - - 如果你允许 compose 再加一条 volume,那最简单仍是:保留 ../../shared:/private,再额外 - 加 ../../shared/common/wandb:/vol(服务端就无需任何改造)。 - diff --git a/specs/mvp/v1.1/mvp_plan.md b/specs/mvp/v1.1/mvp_plan.md deleted file mode 100644 index 99dbbc9..0000000 --- a/specs/mvp/v1.1/mvp_plan.md +++ /dev/null @@ -1,169 +0,0 @@ -# MVP v1.1 计划(Hardening + 多 Workload 可行性验证) - -本目录是 `specs/mvp/v1/` 的下一步迭代:在 v1 已经跑通(Ray head + 2 worker,PPO on Ray,持久化落盘)的基础上,把它升级为**可长期回归**的最小系统,并扩展至少一个新 workload 的可行性闭环。 - -> v1.1 的目标不是做平台服务化(API/队列/多用户)——那是 v2/v3 的工作;v1.1 聚焦“工程化 + 可行性边界验证 + 可观测/可排障基础”。 - ---- - -## 1. v1 基线回顾(已完成) - -- 拓扑:1 head(无 GPU,CPU/GPU=0)+ 2 worker(各 4 GPU) -- 提交方式:必须用 head 上的 `ray job submit` -- driver 调度:通过 `worker_node` 自定义资源 + `--entrypoint-resources` 强制 driver 在 worker -- 输出:按 `submission_id` 组织到共享目录(NFS) - -相关实现参考: - -- 脚本:`src/mvp/v1/` -- 验收动作:`specs/mvp/v1/v1_action.md` -- Roadmap:`specs/mvp/mvp_roadmap.md` - ---- - -## 2. v1.1 目标(必须达成) - -### 2.1 工程化(Hardening) - -1) **JobSpec 标准化(最小)** - - 把“提交 job 需要的参数”收敛成结构化文件: - - Ray 基础配置(YAML):cluster 地址、entrypoint 资源约束、runtime_env 等 - - 训练 JobSpec(YAML):workload 语义与训练参数 - - 至少覆盖:`submission_id`、workload 类型、资源需求、共享根路径、模型/数据路径、输出目录、超时、环境变量注入。 - - v1.1 实现落点(已在 repo 里提供,SDK 方式): - - RayConfig 示例:`src/mvp/v1.1/py/configs/dev.yaml` - - JobSpec 示例:`src/mvp/v1.1/py/jobspecs/{ppo,grpo,sft}.yaml` - - 提交入口:`src/mvp/v1.1/py/run.py`(在 head 容器内执行,使用 Ray Python SDK 提交) - - 设计文档:`specs/mvp/v1.1/sdk_submit_refactor.md` - -2) **共享根路径抽象(dev/prod 一致)** - - 引入 `SHARED_ROOT` 作为唯一共享根路径: - - dev:建议也用 `/private`(docker compose 把宿主机 shared 挂到容器内 `/private`,模拟生产) - - prod:固定 `/private`(算力平台容器内 NFS) - - 任何代码/脚本不得写死 `/mnt/shared`(允许兼容旧路径但不得作为主路径)。 - -3) **共享目录分层(新增 `common/` 与 `user/`)** - - 在 `datasets/hf/jobs/outputs` 之外,新增一个所有用户可读写的共享区: - - `${SHARED_ROOT}/common/`:共享模型/数据/代码快照(多版本 verl / 公共数据) - - `${SHARED_ROOT}/user/`:用户自定义代码(例如 `reward_fn_path` 指向这里) - - v1.1 默认策略:先假设“所有用户可写”(后续 v3 再做权限与隔离)。 - -4) **可排障基础** - - 每个 job 目录必须有: - - `config/`:提交命令、JobSpec 快照、关键 env_vars - - `logs/`:Ray job logs + hydra logs(如有) - - `checkpoints/`:按 `save_freq` 控制频率(默认每 10 step) - - 提供“失败快照”能力:收集 `ray status` / `ray job list` / `ray list nodes` / `ray list actors`(最少其中 2 项)写入 job 目录。 - - v1.1 submitter 默认落盘: - - `${SHARED_ROOT}/jobs//config/job_spec.json` - - `${SHARED_ROOT}/jobs//config/runtime_env.json` - - `${SHARED_ROOT}/jobs//config/submit_cmd.txt` - - `${SHARED_ROOT}/jobs//logs/ray_job_submit.out` - - `${SHARED_ROOT}/jobs//debug/ray_status_{pre,post}.txt` - - `${SHARED_ROOT}/jobs//debug/ray_job_list_post.txt` - -### 2.2 Workload 扩展(至少新增 1 个) - -v1.1 需要新增并验收通过两个 workload(都要跑通闭环): - -- **GRPO on Ray**(推荐优先,复用 PPO 入口,通过算法配置切换) - - 基于 `python -m verl.trainer.main_ppo` - - 通过配置覆盖:`algorithm.adv_estimator=grpo`(以及必要的 rollout 参数) - - - **SFT on Ray(Ray-native)** - - 入口:`python -m verl.trainer.sft_trainer_ray` - - 参考实现:`verl/verl/trainer/sft_trainer_ray.py`(内部会 `ray.init()`) - - 需要确保 `ray.init()` 连接已有集群: - - 优先:`runtime_env.env_vars.RAY_ADDRESS=auto`(配合 `ray job submit`) - - 兜底:在 v1.1 的 launcher 脚本里显式 `ray.init(address="auto")` 再调用 trainer(避免依赖 Ray 的 env var 行为差异) - - 重要细节:Ray Job 的 entrypoint(driver)默认不分配 GPU,因此 SFT driver 侧不要强依赖 CUDA: - - 推荐:`trainer.device=cpu`(driver 只做 orchestration;训练由 Ray workers 占 GPU) - ---- - -## 3. v1.1 关键设计点 - -### 3.1 多版本代码与自定义逻辑(为 v3.1 铺路,但 v1.1 先做最小验证) - -已确定优先方案(A):通过 **Ray Job 的 `runtime_env.env_vars`** 注入 `PYTHONPATH`。 - -- `code_path`(例如 `${SHARED_ROOT}/common/code/verl/`) -- 提交 job 时设置: - - `runtime_env.env_vars.PYTHONPATH = ":$PYTHONPATH"` - -并约定: - -- `reward_fn_path` 可指向 `${SHARED_ROOT}/user/code/...` 下用户自定义代码 -- 与 `code_path` 一样,必须通过 `runtime_env.env_vars` 确保该路径可被 import(例如把 `${SHARED_ROOT}/user/code` 也加入 `PYTHONPATH`) - -v1.1 中至少做一次“代码覆盖验证”: - -- 在 code_path 下放一个可识别的 `verl` 版本标识(例如 `verl.__version__` 打印差异) -- 提交 job 并在日志中确认 import 的是 code_path 的版本(而不是镜像内默认安装) - -v1.1 的最小落地方式(已实现): - -- 提供代码快照脚本:`src/mvp/v1.1/scripts/31_snapshot_verl_code.sh` - - 会把 `/workspace/verl`(挂载的 repo)复制到 `${SHARED_ROOT}/common/code/verl//` - - 并写入 `${code_path}/mvp_marker.py`,用于在 Ray job logs 中验证“选用的是哪份 code_path” -- submitter 会在 entrypoint 前运行 preflight: - - 打印 `verl.__file__` 与 `mvp_marker.MARKER` - - 由此确认 job 粒度的 PYTHONPATH 生效,且不同 job 可指向不同 `code_path`(多版本共存) - -### 3.2 Checkpoint 策略(磁盘保护) - -- 默认:`save_freq=10`(每 10 step 保存一次) -- 对于 step 数已知的短任务(例如 29 steps),可以通过配置把 `save_freq` 调整为 10/15/29(按需求权衡) -- 作业目录按 `submission_id` 隔离,方便清理与归档 - ---- - -## 4. v1.1 交付物清单(代码 + 文档) - -### 4.1 代码(建议落点) - -在 `src/mvp/` 下新增 v1.1 级别的提交器与模板(或在 `src/mvp/v1` 原地演进但要保持 v1 可回归): - -- `src/mvp/v1.1/` - - `docker-compose.yaml`(与 v1 互不干扰的容器名/网络名) - - `scripts/`(Ray 启动/prepare 保留 bash;submit 通过 SDK 工具执行) - - `py/`(工程化提交层:YAML + Ray Python SDK) - - `py/configs/`(Ray 基础配置) - - `py/jobspecs/`(训练 JobSpec) - - `py/run.py`(入口) - -此外,为了对齐 dev 环境约束(远程机固定目录): - -- 远程机目录必须新增:`argus@h1:/home2/argus/infra/mvp/v1.1/` -- 该目录内需包含 v1.1 的全部内容(compose + scripts + README),可由本 repo 的 `src/mvp/v1.1/` 同步过去 - -### 4.2 文档 - -- `specs/mvp/v1.1/v1.1_action.md`:开发、部署、测试、验收流程(可复现) -- 更新 `specs/mvp/mvp_roadmap.md`:保持路线图与落地一致(按需) - ---- - -## 5. v1.1 验收标准(DoD) - -### 5.1 Hardening DoD - -- [ ] 所有提交均由 head 执行 `ray job submit`,且显式 `--submission-id=` -- [ ] 共享根路径由 `SHARED_ROOT` 控制(dev/prod 可切换),脚本无硬编码 -- [ ] 每个 job 的输出目录为:`${SHARED_ROOT}/jobs//` -- [ ] checkpoint 不会“每 step 保存”导致爆盘:默认 `save_freq=10` -- [ ] job 失败时,`${SHARED_ROOT}/jobs//config/` 中有足够信息定位(命令、env、ray 状态快照) - - [ ] v1.1 测试前会清理 v1 的遗留容器/进程(避免端口、容器名、Ray session 干扰) - -### 5.2 Workload DoD(GRPO + SFT 都必须) - -GRPO(必须): - -- [ ] `algorithm.adv_estimator=grpo` 的 job 可提交并进入 RUNNING -- [ ] job 能跑完最小训练步数(可设 `total_epochs=1` 或 `total_training_steps`) -- [ ] 输出目录内有日志与至少 1 次 checkpoint(或明确不保存并说明原因) - -SFT(必须): - -- [ ] `sft_trainer_ray` 可连接集群并跑到至少 1 个 step(推荐最小训练步数/epoch) -- [ ] 输出目录与 checkpoint 策略同 v1.1 规范(落盘到 `${SHARED_ROOT}/jobs//...`) diff --git a/specs/mvp/v1.1/sdk_submit_refactor.md b/specs/mvp/v1.1/sdk_submit_refactor.md deleted file mode 100644 index 9c6412b..0000000 --- a/specs/mvp/v1.1/sdk_submit_refactor.md +++ /dev/null @@ -1,148 +0,0 @@ -# MVP v1.1 工程化重构方案:Ray Python SDK 提交层(YAML Config + YAML JobSpec) - -本文档把 v1.1 的“代码工程化”目标落到一个明确的设计:**保留现有 scripts**(Ray 集群构建、数据准备、模型准备、代码快照),将“任务提交机制”重构为 **Ray Python SDK**(`ray.job_submission.JobSubmissionClient`)驱动的 Python 工具层。 - -> 约束(已确认) -> 1) 基础配置用 YAML,JobSpec 也用 YAML。 -> 2) 工具必须在 **head 容器**执行(从 head 发起提交,满足“在 head 提交”的要求)。 -> 3) 训练参数组织保持与现在一致:仍然使用 **Hydra overrides** 方式构造 entrypoint。 -> 4) 不使用 `requests` 直连 HTTP API(只用 Ray SDK)。 - ---- - -## 1. 当前 Ray SDK 能力验证(关键前提) - -在 head 容器(`mvp11-ray-head`)中验证: - -- Ray 版本:`2.51.1` -- `JobSubmissionClient.submit_job` 支持以下关键字段: - - `submission_id` - - `runtime_env` - - `entrypoint_num_cpus` - - `entrypoint_num_gpus` - - `entrypoint_resources`(用于强制 driver 落 worker) - -因此 v1.1 可以“纯 SDK”完成提交,不需要 `requests` fallback。 - ---- - -## 2. 系统分层(不动 scripts,只重构提交层) - -### 2.1 scripts(保留) - -`src/mvp/v1.1/scripts/` 继续负责: - -- 容器生命周期:`01_up.sh` / `02_down.sh` -- Ray 启动:`20_start_head.sh` / `21_start_workers.sh` -- 数据/模型准备:`30_prepare_data_and_model.sh` -- 代码快照:`31_snapshot_verl_code.sh`(生成 `${SHARED_ROOT}/common/code/verl//`) - -scripts 可以新增一个“薄封装”脚本,负责 `docker exec` 进 head 容器并运行 Python 提交器,但 scripts 不再拼 `ray job submit ...` CLI 字符串。 - -### 2.2 Python 工具层(新增) - -在 `src/mvp/v1.1/py/` 新增提交工具层: - -- 读取 Ray 基础配置(YAML) -- 读取训练 JobSpec(YAML) -- 用 Ray Python SDK 提交/查询/停止/拉日志 -- 将 job 级别产物落盘到:`${SHARED_ROOT}/jobs//...` - ---- - -## 3. 输入定义:两份 YAML - -### 3.1 Ray 基础配置(RayConfig YAML) - -这份配置是“稳定可复用”的,描述 cluster 与 driver placement 等通用信息。 - -字段建议: - -- `address`: `http://127.0.0.1:8265`(从 head 容器内部视角) -- `shared_root`: `/private` -- `entrypoint_num_cpus`: `1` -- `entrypoint_resources`: `{"worker_node": 1}`(强制 driver 使用 worker 才有的资源) -- `runtime_env.env_vars`: HF cache / endpoint 等通用环境变量 -- `user_code_path`: `${shared_root}/user/code`(可选,默认值也可) - -### 3.2 训练 JobSpec(JobSpec YAML) - -这份配置是“一次训练”语义,描述 workload + 训练参数 + code_path 多版本等。 - -字段建议: - -- `workload`: `ppo|grpo|sft` -- `submission_id`: 可选(不填则生成;但最终必须显式传给 SDK) -- `code_path`: `${shared_root}/common/code/verl/`(多版本关键字段) -- `model_id` -- 数据路径:`train_file` / `val_file`(按 workload) -- 训练参数:`nnodes` / `n_gpus_per_node` / `total_training_steps` / `save_freq` / `test_freq` - -注意(SFT 的 driver 设备选择): - -- Ray job 的 entrypoint(driver)默认不分配 GPU(我们通常不设置 `entrypoint_num_gpus`)。 -- `sft_trainer_ray.py` 的 driver 会用 `trainer.device` 做张量统计;若设置为 `cuda` 且 driver 无 GPU,会报: - - `RuntimeError: No CUDA GPUs are available` -- 因此 v1.1 的 SFT JobSpec 默认应设置:`trainer.device=cpu`(训练 workers 仍会占用 GPU)。 - ---- - -## 4. Python 提交器的职责(tool class) - -建议实现 `RayJobTool`(或类似命名),能力: - -### 4.1 submit(核心) - -输入:`RayConfig + JobSpec` -输出:`submission_id` - -实现要点: - -- `client = JobSubmissionClient(address)` -- 生成/确定 `submission_id` -- `runtime_env` 合并逻辑: - - 合并 config 与 jobspec 的 `env_vars` - - 强制注入多版本: - - `PYTHONPATH = "::$PYTHONPATH"` -- 构造 entrypoint(保持 hydra overrides 风格): - - PPO/GRPO:`python3 -m verl.trainer.main_ppo ...` - - SFT:`python3 -m verl.trainer.sft_trainer_ray ...` -- 强制 driver 落 worker: - - `entrypoint_resources=config.entrypoint_resources` - - `entrypoint_num_cpus=config.entrypoint_num_cpus` -- 落盘产物: - - `${shared_root}/jobs//config/{ray_config.yaml,jobspec.yaml,submit_payload.json}` - - `${shared_root}/jobs//logs/submit.out` - - `${shared_root}/jobs//debug/{ray_status_pre,ray_job_list_post}.txt`(可用 SDK 或 `ray status` 采集) - -### 4.2 status / stop / logs / list - -- `status(submission_id)` -- `stop(submission_id)` -- `logs(submission_id)`(可支持 tail) -- `list()` - ---- - -## 5. `run.py` 入口(必须在 head 容器执行) - -建议入口: - -- `python3 /workspace/mvp/v1.1/py/run.py --config --jobspec --action submit` -- `--action` 支持:`submit|status|stop|logs|list` - -host 侧执行方式(由 scripts 薄封装): - -- `docker exec mvp11-ray-head python3 /workspace/mvp/v1.1/py/run.py ...` - ---- - -## 6. 验收口径(工程化部分) - -1) **SDK 提交**:不使用 `ray job submit` CLI,改用 `JobSubmissionClient.submit_job`。 -2) **driver 仍强制在 worker**:SDK 提交时 `entrypoint_resources={"worker_node":1}` 生效。 -3) **多版本共存验证**: - - 通过 `31_snapshot_verl_code.sh` 生成 `codeA/codeB` 两份 code_path - - 通过两份 JobSpec 分别指向不同 `code_path` - - 在 job logs 中看到不同的 marker(例如 `mvp_marker.MARKER`) - diff --git a/specs/mvp/v1.1/v1.1_action.md b/specs/mvp/v1.1/v1.1_action.md deleted file mode 100644 index cd72a7f..0000000 --- a/specs/mvp/v1.1/v1.1_action.md +++ /dev/null @@ -1,333 +0,0 @@ -# MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径) - -本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出**开发改造 → 部署测试 → 验收**的可复现流程。 - -> v1.1 的核心约束(来自讨论结论) -> - 仍然必须通过 **head 节点执行 `ray job submit`** 提交任务。 -> - 训练/driver **必须落在 worker**(head 不跑训练)。 -> - 多版本 `verl` 共存:同一镜像不变,必须通过 **Ray Job `runtime_env.env_vars` 注入 `PYTHONPATH`** 让 job 粒度选择代码版本。 -> - 存储只考虑 NFS:dev 环境我们自己 mount;生产环境容器内统一看到 `/private/`。 - ---- - -## 1. 目标与非目标 - -### 1.1 目标(v1.1 必须做到) - -1) **可回归**:同一环境连续跑多次 PPO 回归,不互相覆盖,输出按 submission id 归档。 -2) **可扩展**:新增并验收通过 2 个 workload(**GRPO + SFT**)并跑通闭环。 -3) **可排障**:每个 job 目录包含完整的提交快照、关键 env、Ray 状态快照与日志入口。 -4) **可多版本共存**:同一 Ray 集群内,不同 job 通过 `PYTHONPATH` 选择不同 `verl` 代码快照。 - -### 1.2 非目标(v1.1 不做) - -- 不做平台 API/队列/多租户/RBAC(这是 v2/v3)。 -- 不做复杂调度(拓扑、IB 域、NUMA、Gang 等自动化策略)。 - ---- - -## 2. 运行环境约定(dev / prod 一致抽象) - -### 2.1 拓扑(单机 3 容器) - -- `mvp-ray-head`:无 GPU,`ray start --head --num-cpus=0 --num-gpus=0`(控制面 only) -- `mvp-ray-worker-0`:4 GPU -- `mvp-ray-worker-1`:4 GPU - -### 2.2 “head 不跑训练”的硬约束实现(必须) - -1) **head CPU=0**:从资源层面阻断默认 task/driver 落到 head。 -2) **worker 自定义资源标签**:worker 启动时带 `--resources='{"worker_node": 100}'`。 -3) **ray job submit 强制 entrypoint 落 worker**:提交时必须带: - - `--entrypoint-resources='{"worker_node": 1}'` - - `--entrypoint-num-cpus=1`(显式声明 driver 需要的 CPU) - -> 验证口径:`ray job list` 的 `driver_info.node_ip_address` 必须是 worker 的 IP,而不是 head IP。 - -### 2.3 共享存储(NFS)与路径(关键) - -- 生产环境:容器内共享根路径固定为 `/private/`(算力平台统一挂载 NFS)。 -- 开发环境:docker compose 也应把宿主机共享目录挂载到容器内的 `/private/`,从而做到 dev/prod 一致。 - -统一约定(容器内视角): - -- `SHARED_ROOT=/private` -- Job 输出:`${SHARED_ROOT}/jobs//` - -建议的共享目录结构(v1.1 新增 `common/` 与 `user/`): - -- `${SHARED_ROOT}/datasets/`:通用数据(例如 gsm8k parquet) -- `${SHARED_ROOT}/hf/`:HuggingFace cache(模型/分词器/权重) -- `${SHARED_ROOT}/jobs/`:按 submission id 归档的作业目录(强制) -- `${SHARED_ROOT}/outputs/`:临时/非强约束输出(不建议长期依赖) -- `${SHARED_ROOT}/ray/`:Ray 调试痕迹(可选,通常 Ray 默认写 `/tmp/ray`) -- `${SHARED_ROOT}/common/`:所有用户可读写共享区(模型/数据/代码快照) - - `${SHARED_ROOT}/common/models/`:可复用基础模型(可用软链指向 hf cache 或 snapshot) - - `${SHARED_ROOT}/common/datasets/`:共享数据(或与 `datasets/` 统一规划) - - `${SHARED_ROOT}/common/code/`:代码快照(多版本 `verl` / 自定义 reward) -- `${SHARED_ROOT}/user/`:用户自定义内容(默认所有用户可写) - - `${SHARED_ROOT}/user/code/`:reward_fn 等自定义 Python 代码 - ---- - -## 3. 开发实施方案(代码改造清单) - -> v1.1 建议新增 `src/mvp/v1.1/`(保持 v1 可回归不被破坏)。 - -### 3.1 JobSpec(最小标准化) - -v1.1 的工程化目标是把“提交机制”迁移到 Ray Python SDK,因此输入拆为两份 YAML: - -1) Ray 基础配置(YAML):address / entrypoint resources / runtime_env 等 -2) 训练 JobSpec(YAML):workload 语义与训练参数(仍由 Hydra overrides 组织) - -训练 JobSpec(YAML)至少包含: - -- `submission_id`:可空;为空时由 submitter 生成(但最终必须显式传给 `ray job submit --submission-id`) -- `workload`:`ppo` / `grpo` / `sft`(v1.1 必须 `ppo` + `grpo` + `sft`) -- `shared_root`:默认 `/private`(容器内路径) -- `code_path`:`verl` 代码快照目录(用于多版本共存) -- `reward_fn_path`(可选):指向 `${shared_root}/user/code/...` 下的 Python 文件或模块入口 -- `model` / `dataset`:必须指向共享存储的持久化路径(避免每次下载/生成) -- `ray`:`address=http://127.0.0.1:8265`(从 head 容器内部视角) -- `resources`: - - `entrypoint_resources={"worker_node":1}` - - `entrypoint_num_cpus=1` -- `trainer_overrides`:训练参数覆盖(v1.1 默认 `total_epochs=1`、`save_freq=10`) -- `env_vars`:会被透传到 `runtime_env.env_vars`(必须包含 `PYTHONPATH` 注入) - -交付物(v1.1 SDK 方式): - -- `src/mvp/v1.1/py/configs/dev.yaml`(Ray 基础配置示例) -- `src/mvp/v1.1/py/jobspecs/{ppo,grpo,sft}.yaml`(训练 JobSpec 示例) -- `src/mvp/v1.1/py/run.py`(入口:使用 Ray Python SDK 提交/查询/停止/拉日志) -- 设计文档:`specs/mvp/v1.1/sdk_submit_refactor.md` - -### 3.2 多版本 `verl` 共存(必须) - -原则:**镜像固定不变**;job 粒度通过 `PYTHONPATH` 选择 `verl` 代码快照。 - -提交时必须注入(runtime_env): - -- `PYTHONPATH=":$PYTHONPATH"`(`CODE_PATH` 放最前面) - -并要求 job 在日志中打印一行确认 import 来源,例如: - -- `python -c "import verl,inspect; print(verl.__file__)"`(或训练入口启动时打印) - -v1.1 具体实现(可复现): - -- 先用 `src/mvp/v1.1/scripts/31_snapshot_verl_code.sh` 生成代码快照目录 `${SHARED_ROOT}/common/code/verl//` - - 该目录里会包含一个 `mvp_marker.py`(`MARKER=`) -- 提交 job 时让 `code_path` 指向该快照目录;submitter 会在 entrypoint 前打印: - - `MVP_PRECHECK_VERL_FILE`(验证 import 来源) - - `MVP_PRECHECK_MARKER`(验证选择的 code_path) - -### 3.3 `submit_job` 工具(组装 ray job submit) - -新增一个提交器(建议 Python,避免复杂 bash quoting): - -- 输入:JobSpec JSON -- 产物: - - 生成/确定 `submission_id` - - 创建 `${SHARED_ROOT}/jobs//config/`、`logs/`、`checkpoints/` - - 写入 `config/job_spec.json`(原样快照) - - 写入 `config/runtime_env.json`(最终用于 submit 的 JSON) - - 写入 `config/submit_cmd.txt`(最终命令行) -- 执行:在 **head 容器内**运行 `ray job submit ...` - -### 3.4 可排障:debug bundle(强制落盘) - -在 job 生命周期的关键节点收集并落盘(至少 2 类): - -- `ray status` -- `ray job list` -- `ray list nodes` -- `ray list actors` - -建议落盘到: - -- `${SHARED_ROOT}/jobs//debug/`(每次收集带时间戳文件名) - -### 3.5 Workload 扩展:GRPO(v1.1 新增闭环) - -优先用与 PPO 相同入口 `python -m verl.trainer.main_ppo`,仅通过配置切换算法: - -- `algorithm.adv_estimator=grpo` -- 其余保持最小可跑:`total_epochs=1`、`save_freq=10` - -### 3.6 Workload 扩展:SFT on Ray(v1.1 必须新增闭环) - -#### 3.6.1 入口与参考实现 - -- 入口:`python -m verl.trainer.sft_trainer_ray` -- 参考代码:`verl/verl/trainer/sft_trainer.py`(非 Ray 版本)与 `verl/verl/trainer/sft_trainer_ray.py`(Ray 版本) - -> v1.1 要验收的是 “SFT on Ray”,因此默认使用 `sft_trainer_ray.py`。 - -#### 3.6.2 连接已有 Ray 集群(必须) - -`sft_trainer_ray.py` 内部直接调用 `ray.init()`,为了确保它连接到**已有集群**(head+workers),v1.1 约定: - -- 提交 job 时通过 `runtime_env.env_vars` 注入:`RAY_ADDRESS=auto` - -如果发现 `ray.init()` 未按预期读取 `RAY_ADDRESS`(Ray 版本差异风险),v1.1 需要提供一个 launcher 兜底: - -- 由 launcher 先显式 `ray.init(address="auto")`,再调用 SFT trainer 逻辑 - -#### 3.6.3 SFT 数据格式(parquet schema) - -`sft_trainer_ray` 默认使用 `MultiTurnSFTDataset`,parquet 中至少需要: - -- `messages` 列:list[dict],dict 至少含 `role`/`content` - -v1.1 的 `prepare` 阶段需要生成并持久化 SFT 数据,例如: - -- `${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` -- `${SHARED_ROOT}/datasets/gsm8k_sft/val.parquet`(可选) - -单条样本的 `messages` 形态示例: - -- `[{ "role": "user", "content": "" }, { "role": "assistant", "content": "" }]` - -> 注意:SFT parquet 不能直接复用 PPO/RL 的 parquet(schema 不同)。 - -#### 3.6.4 重要细节:SFT Ray Driver 不应依赖 GPU - -在 `ray job submit` 模式下,我们的 entrypoint(driver)默认 **不会分配 GPU**(我们只指定了 `--entrypoint-num-cpus=1`,没有指定 `--entrypoint-num-gpus`)。 - -而 `verl/verl/trainer/sft_trainer_ray.py` 的 driver 逻辑里会用 `trainer.device` 来创建 `torch.tensor(..., device=...)` 做统计,如果设置为 `cuda` 且 driver 没有 GPU,会触发: - -- `RuntimeError: No CUDA GPUs are available` - -因此 v1.1 的 SFT on Ray 验收默认要求: - -- `trainer.device=cpu`(driver 只做 orchestration;真正训练仍由 Ray 的 TrainingWorker/资源池占用 GPU) - -### 3.7 v1.1 脚本化交付(必须独立完整) - -`src/mvp/v1.1/` 需要像 v1 一样提供一套完整脚本,确保 v1.1 可独立运行、可回归: - -- `src/mvp/v1.1/docker-compose.yaml`(容器名建议与 v1 区分,避免冲突) -- `src/mvp/v1.1/scripts/00_prereq_check.sh`(含 GPU/目录/NFS/verl 代码检查) -- `src/mvp/v1.1/scripts/01_up.sh` / `02_down.sh`(起停) -- `src/mvp/v1.1/scripts/20_start_head.sh` / `21_start_workers.sh` -- `src/mvp/v1.1/scripts/30_prepare_data_and_model.sh`(包含 PPO 数据 + SFT 数据) -- `src/mvp/v1.1/scripts/40_submit_ppo_epoch1.sh` -- `src/mvp/v1.1/scripts/41_submit_grpo_epoch1.sh` -- `src/mvp/v1.1/scripts/42_submit_sft_minimal.sh` -- `src/mvp/v1.1/scripts/50_status.sh` -- `src/mvp/v1.1/scripts/31_snapshot_verl_code.sh`(多版本 code snapshot) -- `src/mvp/v1.1/scripts/43_submit_jobspec.sh`(通过 JobSpec 提交) -- `src/mvp/v1.1/scripts/12_install_py_deps.sh`(安装 PyYAML 等依赖) -- `src/mvp/v1.1/scripts/44_submit_sdk.sh`(通过 Ray Python SDK + YAML 提交) - ---- - -## 4. 部署与测试流程(dev 环境) - -> dev 环境以远程机目录为例:`argus@h1:/home2/argus/infra/mvp`。v1.1 的所有内容要求放在: -> -> - `argus@h1:/home2/argus/infra/mvp/v1.1/` -> -> 并在该目录中通过脚本使用 `docker exec` 协调容器。 - -### 4.0 清理 v1 环境(必须先做) - -v1 已在 `argus@h1` 部署过容器与 Ray。为保证 v1.1 的可重复测试,开始 v1.1 前必须清理 v1: - -1) 停止并删除 v1 容器(推荐用 v1 的 down 脚本) -2) 确认 `docker ps` 中不再有 v1 的 `mvp-ray-head/mvp-ray-worker-*` - -v1.1 的脚本里也提供了一个 best-effort 清理脚本:`src/mvp/v1.1/scripts/03_cleanup_v1_legacy.sh`(远程目录中同名脚本)。 - -### 4.1 环境准备(一次性 / 幂等) - -1) 目录检查(远程机): - - `${WORKDIR}/shared/` 存在并具备上述子目录(含 `common/`、`user/`) -2) `verl` 代码目录检查: - - `${WORKDIR}/verl` 不存在则执行 `git clone https://github.com/volcengine/verl.git` -3) GPU 可用性检查: - - 设备存在(例如 0-7 可见),并按 worker 容器分配(每个 worker 4 GPU) -4) 模型与数据持久化路径: - - 模型与数据必须落在 `${SHARED_ROOT}` 下;若已存在则跳过下载/生成 - - SFT parquet 同样必须落在 `${SHARED_ROOT}` 下;若已存在则跳过生成 - -### 4.2 启动 Ray 集群(每次测试) - -1) `docker compose up -d` -2) head:`ray start --head --num-cpus=0 --num-gpus=0 ...` -3) workers:`ray start --address=:6379 --resources='{"worker_node":100}' ...` -4) 验证:`ray status` 显示 1 head + 2 worker,且 head `CPU:0 GPU:0` - -### 4.3 提交 PPO 回归(必须跑 2 次) - -1) 生成 JobSpec(可用模板 + 覆盖项) -2) 在 head 容器内执行 submitter(或直接 `ray job submit`) -3) 验证要点: - - `ray job list`:driver node 是 worker - - `${SHARED_ROOT}/jobs//` 下存在 `config/`、`logs/`、`checkpoints/` - - checkpoint 每 10 step 产生(例如 `global_step_10`) - -### 4.4 提交 GRPO(新增 workload 验收) - -同 PPO,但覆盖 `algorithm.adv_estimator=grpo`,确保能进入 RUNNING 并完成最小步数。 - -### 4.5 提交 SFT on Ray(新增 workload 验收,必须) - -1) 确认 `${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` 已存在(由 v1.1 prepare 生成)。 -2) 通过 head 容器执行 `ray job submit` 提交 `python -m verl.trainer.sft_trainer_ray`。 -3) 关键约束: - - `runtime_env.env_vars.RAY_ADDRESS=auto`(连接已有集群) - - `--entrypoint-resources='{"worker_node": 1}'`(driver 落 worker) - - `PYTHONPATH=:$PYTHONPATH`(多版本 verl) -4) 最小化训练配置建议(避免 OOM/耗时过长): - - `trainer.total_epochs=1` - - `trainer.total_training_steps=10~30` - - `trainer.save_freq=10` - - `trainer.nnodes=2`、`trainer.n_gpus_per_node=4`(用满 8 卡做一次最小分布式验证) - - `data.train_files=${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` - - `trainer.default_local_dir=${SHARED_ROOT}/jobs//checkpoints` - -### 4.6 工程化验证:JobSpec + 多版本共存(v1.1 必须) - -1) 生成两个 code snapshot(不同 `CODE_ID`): - - `CODE_ID=codeA ./scripts/31_snapshot_verl_code.sh` - - `CODE_ID=codeB ./scripts/31_snapshot_verl_code.sh` -2) 分别修改/复制 JobSpec 模板,使 `code_path` 指向不同 snapshot: - - `${SHARED_ROOT}/common/code/verl/codeA` - - `${SHARED_ROOT}/common/code/verl/codeB` -3) 用 JobSpec 提交(必须从 head): - - `./scripts/43_submit_jobspec.sh /workspace/mvp/v1.1/templates/ppo.json`(示例) -4) 在 Ray job logs 中验证: - - `MVP_PRECHECK_MARKER` 打印为对应的 `codeA`/`codeB` - - `MVP_PRECHECK_VERL_FILE` 指向 `${SHARED_ROOT}/common/code/verl/...` 而不是镜像内 site-packages - ---- - -## 5. 验收标准(Definition of Done) - -### 5.1 Hardening DoD(全部必选) - -- [ ] 提交必须来自 head:能在 head 容器内看到 `ray job submit ...` 的提交记录 -- [ ] driver 不在 head:`ray job list` 的 `driver_info.node_ip_address` ∈ worker IP,且 ≠ head IP -- [ ] 输出目录按 submission id 隔离:`${SHARED_ROOT}/jobs//` 不复用、不覆盖 -- [ ] 数据/模型持久化:再次提交时不重复下载/生成(有 “skip if exists” 的日志) -- [ ] checkpoint 策略有效:默认 `save_freq=10`,不会每 step 保存爆盘 -- [ ] debug bundle 落盘:`${SHARED_ROOT}/jobs//debug/` 至少包含 2 类 Ray 状态快照 -- [ ] 多版本共存验证通过:日志中能确认 `verl` import 来源来自 JobSpec 指定的 `code_path` - -### 5.2 Workload DoD(GRPO + SFT 都必须) - -- [ ] GRPO job 能提交、RUNNING、完成最小训练步数 -- [ ] GRPO job 产物目录满足与 PPO 相同的目录规范与 debug 规范 -- [ ] SFT job 能提交、连接已有集群并跑到至少 1 个 step(建议最小步数/epoch) -- [ ] SFT job 产物目录满足与 PPO 相同的目录规范与 debug 规范 - ---- - -## 6. 生产环境部署注意事项(v1.1 需要考虑但不强制在 dev 全量模拟) - -- 容器由算力平台创建:我们只负责 SSH 进去纳管(启动 ray / 提交 job / 收集产物)。 -- 容器内共享路径为 `/private`:所有脚本必须以 `SHARED_ROOT=/private` 工作,不得写死 `/mnt/shared`。 -- 认证仅内部 token:在 submitter 中把 token 作为 env var 透传(不写入日志明文)。 diff --git a/specs/mvp/v1/mvp_plan.md b/specs/mvp/v1/mvp_plan.md deleted file mode 100644 index 026e458..0000000 --- a/specs/mvp/v1/mvp_plan.md +++ /dev/null @@ -1,120 +0,0 @@ -# MVP 计划(V1) - -本文档目标:把当前“口述/实验记录”整理成**可复现、可验收**的 MVP 计划,并明确下一步最小闭环。 - -## 1. 背景与目标 - -我们要验证的最小闭环是: - -1) 在“Head(CPU 容器)+ Worker(GPU 容器)”的 Ray Cluster 上,能够跑通一次 `verl` 的 PPO 训练。 -2) 训练所需的 **数据集 / 模型缓存 / 训练产物(checkpoint)/ 日志** 不落在容器临时文件系统里,而是落在**共享存储(NFS)**,容器重启后可继续使用。 -3) 所有步骤能写成一套**清晰命令/脚本**,新人可照着复现。 - -## 2. 环境与假设 - -- 机器:H20 机器(具体规格由算力平台提供) -- 访问方式:通过 `ssh h1a` 远程登录(进入算力平台/宿主访问入口) -- 容器:算力平台可申请 CPU 容器(对外暴露端口)与若干 GPU 容器(可 SSH 互通) -- 共享存储:所有容器可挂载同一套 NFS(在 `specs/hl_design_v2.md` 中假设为 `/mnt/shared`) - -## 3. 已验证现状(现有实验) - -目录 `ray_in_docker/` 已经做过一次可运行的实验(偏“本地/示例级别”): - -- 用 `docker-compose` 起了 2 个 `verl` 镜像容器: - - `verl-head`:作为 Ray Head(Dashboard 端口 `8265`) - - `verl-worker`:作为 Ray Worker -- 在容器中执行: - - 下载 GSM8K 数据集(`examples/data_preprocess/gsm8k.py`) - - 拉取 HuggingFace 模型(示例:`Qwen/Qwen2.5-0.5B-Instruct`) - - `ray start --head` + `ray start --address=...` - - 通过 `ray job submit ... python -m verl.trainer.main_ppo ...` 提交 PPO 训练任务(见 `ray_in_docker/ray_example/ppo_train.sh`) - -结论:**训练脚本可以跑通**。 - -## 4. 当前主要问题(从实验到平台化 MVP 的差距) - -1) **数据 / 模型 / 输出落在容器内**:容器重启/替换后不可复用;也不利于多人共享与审计。 -2) **缓存路径不规范**:HuggingFace cache、Ray 临时目录、Hydra 输出目录等可能分散在容器默认路径。 -3) **可复现不足**:缺少明确的目录规范、统一的启动/提交流程、验收口径。 -4) Ray 节点**打标签/亲和性调度**的方法未固化:需要明确是否统一用 `ray start --resources`,以及命名规范如何设计。 - -## 5. MVP V1:最小闭环定义 - -以 `specs/hl_design_v2.md` 的方向为准,但 V1 **只做最小可运行原型**,暂不做完整 Web/调度系统。 - -### 5.1 目录规范(统一落到 NFS) - -约定所有容器统一挂载 NFS 到 `/mnt/shared`,并在其中固定目录结构: - -- `/mnt/shared/code/`:代码(可选:按版本/分支隔离) -- `/mnt/shared/datasets/`:数据集(如 `gsm8k/`) -- `/mnt/shared/hf/`:HuggingFace 缓存(设置 `HF_HOME=/mnt/shared/hf`) -- `/mnt/shared/ray/`:Ray 运行期临时目录(可选:设置 `RAY_TMPDIR=/mnt/shared/ray/tmp`) -- `/mnt/shared/outputs/`:训练输出根目录(Hydra/日志/ckpt 统一落这里) - - `/mnt/shared/outputs/logs//` - - `/mnt/shared/outputs/checkpoints//` - -### 5.2 最小集群形态 - -- 1 个 Head(CPU 容器) - - 跑 `ray start --head --dashboard-host=0.0.0.0` - - 暴露 `8265` 给 Desktop/用户查看 Job 状态 -- 1~2 个 Worker(GPU 容器) - - 跑 `ray start --address=:6379` 加入集群 - - (可选)通过 `--resources='{\"gpu_pool_a\": 1}'` 给节点打标签 - -### 5.3 最小训练任务 - -- 目标任务:跑通一次 `verl.trainer.main_ppo`(以 GSM8K 为例) -- 要求: - - `data.train_files` / `data.val_files` 指向 `/mnt/shared/datasets/...` - - HuggingFace 模型下载缓存落到 `/mnt/shared/hf` - - 训练输出(Hydra outputs、checkpoint、stdout/stderr)落到 `/mnt/shared/outputs/...` - -建议在提交命令里显式覆盖 Hydra 输出目录(示例,具体目录名按需调整): - -- `hydra.run.dir=/mnt/shared/outputs/logs/${JOB_TAG}` - -## 6. 实施步骤(Checklist) - -### 6.1 一次性准备 - -- [ ] 确认所有容器已挂载 NFS 到同一路径:`/mnt/shared` -- [ ] 在 `/mnt/shared/` 下创建目录:`datasets/ hf/ outputs/ ray/` -- [ ] 在所有容器中设置/注入环境变量(推荐写入统一脚本): - - `HF_HOME=/mnt/shared/hf` - - `HF_ENDPOINT=https://hf-mirror.com`(如需) - - `RAY_TMPDIR=/mnt/shared/ray/tmp`(可选) - -### 6.2 启动集群(Head + Worker) - -- [ ] 在 Head 容器启动 Ray Head(并记录 `head_ip:6379`) -- [ ] 在每个 Worker 容器执行 `ray start --address=...` 加入集群 -- [ ] 在 Head 上通过 `ray status` / Dashboard 验证节点已注册 - -### 6.3 准备数据与模型 - -- [ ] 数据集下载到:`/mnt/shared/datasets/gsm8k/` -- [ ] 模型缓存落到:`/mnt/shared/hf/`(拉取一次即可多任务复用) - -### 6.4 提交训练任务 - -- [ ] 用 `ray job submit --address=http://:8265 ...` 提交 PPO 训练 -- [ ] 训练日志与 checkpoint 在 `/mnt/shared/outputs/` 可见 - -## 7. 验收标准(V1) - -- [ ] Ray Head/Worker 能稳定加入同一集群(Dashboard 可见) -- [ ] PPO 训练任务可提交并跑通(至少完成若干 step/epoch) -- [ ] 数据集、HF 缓存、训练输出均在 `/mnt/shared/` 下可复用(容器重启后仍在) -- [ ] 有一份“从零到跑通”的命令清单(或脚本)可复现 - -## 8. 未决问题(记录待补齐) - -- [ ] Ray 节点标签/亲和性调度:是否统一用 `ray start --resources`,以及命名规范如何设计 -- [ ] RL workload 的 Driver 放置策略:先按 `verl` 默认即可,后续再按 `specs/hl_design_v2.md` 收敛到“Driver-on-Head / Placement Group”等模式 - -## 9. 下一步(进入 V2) - -当 V1 达到“可复现 + 产物可落盘”的验收标准后,下一阶段工作见:`specs/mvp_plan_v2.md`。 diff --git a/specs/mvp/v1/v1_action.md b/specs/mvp/v1/v1_action.md deleted file mode 100644 index 31ae03d..0000000 --- a/specs/mvp/v1/v1_action.md +++ /dev/null @@ -1,111 +0,0 @@ -# MVP V1 远程实验行动文档(待确认后执行) - -## 1. 任务复述(我理解的需求) - -你希望我在远程机器 `argus@h1` 上,进入目录 `/home2/argus/infra/mvp`,把 MVP V1 的“原本流程”**手动完整跑一遍并验证**。要求: - -1) 在宿主机上编写脚本,脚本通过 `docker exec` 在容器内执行命令,负责协调启动顺序(先 head、后 worker)。 -2) 集群拓扑改为: - - 1 个 Ray Head:**没有 GPU**,并且 Head 的 Ray 资源 `CPU=0`(防止 Ray 把训练任务调度到 head)。 - - 2 个 Ray Worker:各自 **4 GPU**(总 8 GPU)。 -3) PPO 训练需要“轻量化”,把 `total_epochs` 改为 `1`。 -4) 先在本地仓库 `src/mvp/v1/` 写好脚本与 compose 文件;再拷贝到远程目录执行与验证。 -5) 在你确认这份行动文档没问题之前,我**不执行**远程操作。 - -## 2. 本地已准备的文件(在本仓库内) - -- `src/mvp/v1/docker-compose.yaml`:3 容器(head + 2 worker),head 不使用 nvidia runtime;worker0/1 各限制 4 GPU。 -- `src/mvp/v1/scripts/`:宿主机脚本(内部全部用 `docker exec`) - - `01_up.sh`:起容器 - - `20_start_head.sh`:启动 Ray head(`--num-cpus=0 --num-gpus=0`) - - `21_start_workers.sh`:启动 Ray worker 加入集群 - - `30_prepare_data_and_model.sh`:准备 GSM8K 数据与预下载模型 - - `40_submit_ppo_epoch1.sh`:提交 PPO(`trainer.total_epochs=1`,并设置 `nnodes=2, n_gpus_per_node=4`) - - `run_all.sh`:按顺序一键执行 - -## 3. 远程环境前置条件(需要你确认/保证) - -在 `argus@h1` 上: - -- Docker 可用,且有 `docker compose` 插件(Compose v2)。 -- NVIDIA runtime 可用(worker 容器需要 `runtime: nvidia`),宿主机有至少 8 张 GPU。 -- 不强制要求提前准备 `./verl`:脚本会在宿主机侧检查 `${PWD}/verl`,如果不存在会自动执行: - - `git clone https://github.com/volcengine/verl.git` - -此外本实验默认写入持久化目录:`/home2/argus/infra/mvp/shared`(会自动创建)。 - -## 4. 拷贝到远程(我执行前会再次征求你确认) - -从本地(本机)同步到远程: - -1) 同步脚本与 compose: - - `rsync -av ./src/mvp/v1/ argus@h1:/home2/argus/infra/mvp/src/mvp/v1/` - - `rsync -av ./specs/mvp/v1_action.md argus@h1:/home2/argus/infra/mvp/specs/mvp/v1_action.md` -2) `verl/` 默认不需要同步(远程会 clone)。如果你更希望固定版本/避免网络波动,也可以手动同步: - - `rsync -av --delete ./verl/ argus@h1:/home2/argus/infra/mvp/verl/` - -## 5. 远程执行步骤(在宿主机上) - -在远程机器执行: - -1) 进入目录: - - `cd /home2/argus/infra/mvp` -2) 确保脚本可执行(首次同步后需要做一次): - - `chmod +x ./src/mvp/v1/scripts/*.sh` -3) 启动容器: - - `./src/mvp/v1/scripts/01_up.sh` -4) 安装 editable 版 `verl`(保证 `python -m verl...` 可用): - - `./src/mvp/v1/scripts/10_install_verl_editable.sh` -5) 启动 Ray Head(禁止调度到 head): - - `./src/mvp/v1/scripts/20_start_head.sh` -6) 启动两个 Ray Worker 加入集群: - - `./src/mvp/v1/scripts/21_start_workers.sh` -7) 准备数据 + 预下载模型(落到 `./shared`): - - `./src/mvp/v1/scripts/30_prepare_data_and_model.sh` -8) 提交 PPO(`total_epochs=1`,必须用 `ray job submit` 在 head 提交;通过 `--entrypoint-resources` 强制 driver 调度到 worker): - - `./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh` -9) 观察状态: - - `./src/mvp/v1/scripts/50_status.sh` - - 打开 Ray Dashboard:`http://:8265` - -也可以一键跑: -- `./src/mvp/v1/scripts/run_all.sh` - -## 6. 验收与验证点(执行时我会逐项检查) - -1) Head 节点无 GPU:在 head 容器内 `nvidia-smi` 应不可用或无设备(worker 内可见 4 张)。 -2) Head 的 Ray 逻辑资源为 `CPU=0, GPU=0`:head 不应承载训练任务调度资源(通过 `ray start --num-cpus=0 --num-gpus=0`)。 -3) 集群节点数量正确:`ray status` 中应看到 1 head + 2 worker。 -4) PPO driver 不在 head:`ray job list` 里该 `submission_id` 的 `driver_info.node_ip_address` 应该是 worker 的 IP(`172.19.0.3/172.19.0.4`),不能是 head(`172.19.0.2`)。 -5) PPO 训练只跑 1 个 epoch:提交参数包含 `trainer.total_epochs=1`。 -6) checkpoint 落盘:`/mnt/shared/jobs//checkpoints/` 有产物(脚本通过 `trainer.default_local_dir` 强制指向该目录;不设置 `trainer.default_hdfs_dir`)。 -7) 数据与缓存落盘:`/home2/argus/infra/mvp/shared/` 下出现 datasets/hf/jobs 等目录。 - -补充(磁盘保护): -- checkpoint 不要每步保存(会非常占空间);当前脚本默认 `trainer.save_freq=10`(每 10 step 保存一次)。 - -## 10. 目录命名约定(submission id) - -- 脚本默认会显式指定 `ray job submit --submission-id=$SUBMISSION_ID`,并使用同一个值作为输出目录名: - - 输出目录:`/mnt/shared/jobs/$SUBMISSION_ID/` -- 你可以在提交时自定义 ID(推荐这样便于检索): - - `SUBMISSION_ID=my_run_20251219_001 ./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh` - -## 7. 风险点与兜底 - -- 如果 `runtime: nvidia` 在该环境不生效:需要改成 compose 的 `gpus:` 写法(我会按远程 docker 版本调整)。 -- 如果 Ray Jobs 的 driver 必须在 head 启动(Ray 机制如此):这不影响“训练任务不调度到 head”,但 head 仍会有一个 job driver 进程。 -- 如果 `verl` 在镜像内已安装但版本不匹配:脚本会优先 `pip install -e /workspace/verl` 以保证行为一致。 - -## 8. 你需要确认的 3 个问题(你已确认,我按此执行) - -1) `verl/`:脚本会在远程自动 `git clone https://github.com/volcengine/verl.git`(如你希望固定版本,可改成同步或 checkout tag/commit)。 -2) GPU:`0-7` 可用(worker0 用 `0-3`,worker1 用 `4-7`)。 -3) PPO:用满 8 GPU(`nnodes=2, n_gpus_per_node=4`)。 - -## 9. 你新增的关键要求(我已纳入脚本) - -- 数据与模型必须落在 `/mnt/shared`(由宿主机 `./shared` bind mount 提供),并且具备**幂等**: - - 数据:如果 `train.parquet/test.parquet` 已存在则跳过下载。 - - 模型:优先检测本地 cache(`HF_HOME=/mnt/shared/hf`);存在则跳过,否则才下载。 -- 提交 job 时显式注入 `HF_HOME/HUGGINGFACE_HUB_CACHE/TRANSFORMERS_CACHE`,确保训练使用持久化缓存与数据路径。 diff --git a/specs/mvp/v2.0/v2_api.md b/specs/mvp/v2.0/v2_api.md deleted file mode 100644 index 3e859b7..0000000 --- a/specs/mvp/v2.0/v2_api.md +++ /dev/null @@ -1,194 +0,0 @@ -# MVP v2.0 API 设计(最小可用) - -v2.0 的 API 目标是:把 v1.1 的“脚本提交”变成“服务化提交”,并在服务侧实现队列/重试/状态聚合。 - -约束: -- 内部 token 鉴权(简单即可)。 -- Ray Job 提交必须使用 **Ray Python SDK**(`JobSubmissionClient`),不使用 `requests` 手写 HTTP。 -- 输出与状态必须落盘到 NFS(容器内 `/private`)。 - ---- - -## 1. 鉴权 - -- Header:`Authorization: Bearer ` -- v2.0 不做用户体系与权限隔离;token 只是“防误用”。 -- 配置建议:复用 `src/mvp/v1.1/py/configs/dev.yaml` 并在 `v2.auth.token_env` 指定 token 环境变量名。 - -## 1.1 运行位置(dev 示例) - -- 服务进程运行在 **Ray head 容器**(便于访问 Ray Job server)。 -- 宿主机侧用脚本控制(`docker exec`): - - `src/mvp/v2.0/scripts/20_start_api.sh` - - `src/mvp/v2.0/scripts/21_stop_api.sh` - - `src/mvp/v2.0/scripts/22_status_api.sh` -- 远程机目录约定(示例):`argus@h1:/home2/argus/infra/mvp/v2/`,容器内挂载到 `/workspace/mvp/v2/`。 - ---- - -## 2. 资源与 ID 约定 - -### 2.1 task_id(服务层主 ID) - -- 格式建议:`mvp2----` - - 示例:`mvp2-ppo-20251223-143201-7f3a` - -### 2.2 ray_submission_id(attempt 级 ID) - -- 由 service 派生:`--a` - - 示例:`mvp2-ppo-20251223-143201-7f3a--a01` - -好处: -- Ray 的 submission id 自带 task_id,可直接从 Ray dashboard 反查到服务侧任务。 -- `/private/jobs//...` 目录天然隔离且可读。 - ---- - -## 3. JobSpec(请求体) - -v2.0 **要求 JobSpec 使用 v1.1 同款 YAML**(字段与语义保持一致),服务端接收 YAML 文本并解析后入库(同时原样保存 `jobspec_yaml` 便于审计/复现)。 - -最小字段(示例 YAML): - -```yaml -workload: "ppo" -submission_id: "" # v2.0 服务端会忽略/覆盖(由 task_id 派生 ray_submission_id) -code_path: "/private/common/code/verl/verl_repo" -model_id: "Qwen/Qwen2.5-0.5B-Instruct" -train_file: "/private/datasets/gsm8k/train.parquet" -val_file: "/private/datasets/gsm8k/test.parquet" -nnodes: 2 -n_gpus_per_node: 4 -total_epochs: 1 -total_training_steps: 10 -save_freq: 10 -test_freq: -1 -trainer_device: null # 仅 sft 使用(通常 "cpu") -``` - -说明: -- `trainer_device` 仅对 `sft` 生效(通常为 `cpu`,避免 driver 无 GPU)。 -- `val_file` 可为 `null`(例如 SFT)。 - ---- - -## 4. API 端点 - -### 4.1 提交任务 - -`POST /api/v2/tasks` - -Request body: -- **raw JobSpec YAML**(与 v1.1 jobspec YAML 结构一致) - -Headers: -- `Content-Type: application/yaml`(或 `text/yaml`) - -Response: -```json -{ - "task_id": "mvp2-ppo-20251223-143201-7f3a", - "state": "QUEUED" -} -``` - -### 4.2 查询任务(聚合状态) - -`GET /api/v2/tasks/{task_id}` - -Response(示例): -```json -{ - "task_id": "mvp2-ppo-20251223-143201-7f3a", - "workload": "ppo", - "state": "RUNNING", - "desired_resources": {"nnodes": 2, "n_gpus_per_node": 4, "total_gpus": 8}, - "latest_attempt": { - "attempt_no": 1, - "ray_submission_id": "mvp2-ppo-20251223-143201-7f3a--a01", - "ray_status": "RUNNING", - "start_time": "2025-12-23T14:32:10+08:00" - }, - "error_summary": null -} -``` - -### 4.3 列出 attempts - -`GET /api/v2/tasks/{task_id}/attempts` - -Response: -```json -{ - "task_id": "mvp2-ppo-20251223-143201-7f3a", - "attempts": [ - { - "attempt_no": 1, - "ray_submission_id": "mvp2-ppo-20251223-143201-7f3a--a01", - "ray_status": "FAILED", - "failure_kind": "INSUFFICIENT_RESOURCES", - "message": "Total available GPUs 0 is less than total desired GPUs 8", - "start_time": "...", - "end_time": "..." - } - ] -} -``` - -### 4.4 取消任务 - -`POST /api/v2/tasks/{task_id}:cancel` - -行为: -- 若 task 处于 `SUBMITTED/RUNNING`:调用 Ray Jobs SDK `stop_job(ray_submission_id)` 并标记 `CANCELED` -- 若处于 `QUEUED/PENDING_RESOURCES`:直接标记 `CANCELED`(不提交) - -Response: -```json -{"task_id":"...","state":"CANCELED"} -``` - -### 4.5 获取日志 - -`GET /api/v2/tasks/{task_id}/logs?attempt=latest&tail=2000` - -返回: -- `text/plain`(直接透传 Ray Job logs tail) - -说明: -- v2.0 先用 Ray SDK `get_job_logs()`。 -- 若需要更稳定的归档,可在 scheduler 定期抓取并落盘(v2.1+)。 - -### 4.6 列出队列(运维/调试) - -`GET /api/v2/queue` - -Response: -```json -{ - "pending": [{"task_id":"...","state":"PENDING_RESOURCES","next_run_at":"..."}], - "running": [{"task_id":"...","ray_submission_id":"..."}] -} -``` - ---- - -## 5. 错误码(最小) - -- `400`:jobspec 缺字段/非法 -- `401`:token 不正确 -- `404`:task 不存在 -- `409`:状态冲突(例如已终态又 cancel) -- `500`:服务内部错误 - ---- - -## 6. SQLite 持久化(API 可见性) - -v2.0 服务端使用 SQLite 持久化保存: -- tasks(`task_id`、`state`、`jobspec_yaml`、`next_run_at`、`latest_attempt_no` 等) -- attempts(`ray_submission_id`、`ray_status`、失败原因等) - -因此: -- `GET /api/v2/tasks/{task_id}` 的数据来自 SQLite(再叠加 Ray 状态同步的结果)。 -- 进程重启后,队列可恢复,`PENDING_RESOURCES` 的任务会在 `next_run_at` 到期后继续尝试提交。 diff --git a/specs/mvp/v2.0/v2_plan.md b/specs/mvp/v2.0/v2_plan.md deleted file mode 100644 index 4b0a27e..0000000 --- a/specs/mvp/v2.0/v2_plan.md +++ /dev/null @@ -1,306 +0,0 @@ -# MVP v2.0 开发计划(服务化入口 + 队列调度 + Ray Jobs SDK) - -目标:在 v1.1(脚本 + Ray Jobs SDK)已验收通过的基础上,交付一个**可独立运行的最小“服务层”**: -- 用户通过 **HTTP API** 提交训练任务(PPO/GRPO/SFT)。 -- 服务层分配一个**人类易读的任务 ID**(`task_id`),并把任务放入队列。 -- 后台调度器在资源满足时再向 Ray 集群提交 Ray Job,并持续追踪 Ray Job 状态。 -- 针对 `verl` 的 **fail-fast 资源预检查**(资源不足直接 `ValueError` 失败)做“服务级重试/排队”,避免用户反复手工提交。 - -> 约束继承 v1.1:head 不跑训练;driver 必须落到 worker;共享存储只考虑 NFS(容器内 `/private`)。 - ---- - -## 1. 背景:为什么 v2.0 需要“服务层调度” - -在 v1.1 中我们通过 Ray Job 提交 `verl` 训练任务。`verl` PPO/GRPO 在初始化 worker 时会创建资源池,并做一次 fail-fast 的资源检查: -- 触发点:`ResourcePoolManager.create_resource_pool()` 末尾调用 `_check_resource_available()` -- `_check_resource_available()` 使用 `ray._private.state.available_resources_per_node()` 统计“可用 GPU/NPU”,如果不足则直接抛异常: - - `ValueError: Total available GPUs 0 is less than total desired GPUs 8` - -这是一种合理的选择(避免 Ray 层面无限 pending/卡死),但会带来一个平台侧问题: -- 当集群暂时没有足够资源时,用户提交会“立刻失败”,需要手动重试。 - -因此 v2.0 的服务层要提供: -- **队列 + gang 约束**:资源不满足则任务在服务层 pending(不提交到 Ray)。 -- **状态追踪**:一旦提交到 Ray,持续获取 Ray Job 状态并回传给用户。 -- **资源不足的“自动重试”**:即使发生 race(提交时资源够、启动时被抢走),也能识别该类失败并延迟重试。 - ---- - -## 2. v2.0 交付范围(Scope) - -### 2.1 必做(MVP v2.0) - -1) **HTTP API**(内部 token): - - 提交任务、查询任务、取消任务、拉取日志(最小可用)。 -2) **任务队列与调度器**: - - FIFO(先到先服务),无配额/公平性(留给 v3+)。 - - gang:按 `nnodes` + `n_gpus_per_node` 的固定资源需求“全有才提交”。 -3) **Ray Jobs SDK 集成**(不使用 `requests` 自己拼 HTTP): - - 通过 `ray.job_submission.JobSubmissionClient` submit/status/stop/logs。 -4) **可观测/可排障最小集**: - - 每个 task/attempt 落盘配置、提交载荷、Ray 返回的 `submission_id`、关键日志。 -5) **失败策略**: - - 识别 “资源不足 fail-fast” 类失败 → 转为 `PENDING_RESOURCES` 并延迟重试。 - - 其他失败保持 `FAILED`(不自动重试,避免掩盖错误)。 - -### 2.2 不做(v2.0 不实现) - -- 多租户/配额/优先级/公平性调度(v3)。 -- Pipeline(多 job 串联)(v3+)。 -- 完整 UI(v3+,v2.0 可只提供 OpenAPI/Swagger)。 -- K8s 编排(明确不做,仍是 Native Ray)。 - ---- - -## 2.3 工程原则(开闭原则 / 复用 v1.1) - -v2.0 研发遵循开闭原则(Open/Closed Principle): -- **对扩展开放**:新增“服务层(API + scheduler + SQLite)”能力以支持排队、重试、状态聚合。 -- **对修改关闭**:尽量不改动 v1.1 已经稳定可用的 Ray Jobs SDK 提交链路代码。 - -落地方式: -- 将 `src/mvp/v1.1/py/mvp_v11/` 作为“成熟可用提交层”,原样拷贝到 `src/mvp/v2.0/py/mvp_v11/` 供 v2.0 复用。 -- v2.0 的新增功能全部在新模块实现(例如 `src/mvp/v2.0/py/mvp_v2/`),通过组合/封装来调用 `mvp_v11`,避免在旧代码中掺杂平台逻辑。 - ---- - -## 3. 总体架构(v2.0) - -### 3.1 组件 - -- **mvp-api**(HTTP Server) - - 接收 JobSpec(结构化字段保持与 v1.1 一致的语义) - - 生成 `task_id` 并写入持久化 - - 提供 query/cancel/logs - -- **mvp-scheduler**(后台调度器,可与 api 同进程也可拆进程) - - 轮询队列:对 `PENDING_RESOURCES` 的任务做资源判断 - - 资源满足 → 调用 Ray Jobs SDK 提交 → 记录 `ray_submission_id` - - 对 `SUBMITTED/RUNNING` 的任务持续同步 Ray Job 状态 - - 如果 Ray Job 失败且命中资源不足模式 → 延迟重试 - -> 部署建议:v2.0 先在 **head 容器**内运行该服务(dev/prod 行为一致;生产环境只能 ssh 进入容器纳管)。 - -### 3.4 dev 环境目录约定(示例) - -以当前远程开发机为例(`argus@h1`): -- 宿主机目录:`/home2/argus/infra/mvp/v2/` -- 容器内挂载:`/workspace/mvp/v2/` -- 共享 NFS:容器内统一为 `/private/`(与 v1.1 保持一致) - -> 注意:服务脚本(`v2/scripts/*.sh`)应在**宿主机**执行,通过 `docker exec` 控制 head 容器;训练 driver 仍通过 Ray entrypoint_resources 强制落到 worker。 - -### 3.2 与 Ray/容器的关系 - -- 服务进程运行在 head(或等价能访问 head 的 Job server 地址)。 -- 提交时仍使用 v1.1 的强约束: - - head:`--num-cpus=0 --num-gpus=0` - - worker:`--resources='{\"worker_node\": 100}'` - - job entrypoint:`entrypoint_resources={\"worker_node\": 1}` 强制 driver 落 worker - ---- - -## 3.3 配置约定(复用 v1.1 dev.yaml 并扩展) - -v2.0 的服务层(API + scheduler)建议复用 v1.1 已存在的 RayConfig 文件: -- `src/mvp/v1.1/py/configs/dev.yaml` - -原因: -- 其中已包含 v1.1 运行所需的 Ray 基础配置(Ray Job server address、entrypoint_resources、runtime_env 等),v2.0 也需要同样的信息来提交 Ray Jobs。 - -扩展方式: -- 在该 YAML 中新增一个顶层 `v2:` section,存放 v2 服务专属配置(API 监听、SQLite 路径、scheduler 间隔等)。 -- v1.1 submitter 只读取 `address/shared_root/entrypoint_* /runtime_env/user_code_path`,会忽略 `v2:` 之类的额外字段;因此不会破坏 v1.1。 - -最小新增项建议(示例): -- `v2.api.host` / `v2.api.port` -- `v2.auth.token_env`(内部 token 环境变量名) -- `v2.sqlite.db_path`(建议 `/private/common/db/mvp_v2.sqlite3`) -- `v2.scheduler.tick_s` / `v2.scheduler.retry_interval_s` / `v2.scheduler.max_running_tasks` - ---- - -## 4. 核心数据模型(Task / Attempt) - -### 4.1 Task(用户视角的任务) - -- `task_id`:**人类易读**且唯一,例如: - - `mvp2-ppo-20251223-143201-7f3a` -- `workload`:`ppo|grpo|sft` -- `jobspec`:提交参数(**保持 v1.1 的 jobspec YAML 字段与语义**;服务端解析 YAML 后入库) -- `state`:见第 5 节状态机 -- `created_at` / `updated_at` -- `latest_attempt`:指向当前 attempt -- `attempts[]`:历史尝试列表 -- `error_summary`:面向用户的简短错误(最后一次失败原因) - -### 4.2 Attempt(一次真实的 Ray Job 提交) - -- `attempt_no`:从 1 开始递增 -- `ray_submission_id`:建议派生自 task_id: - - `ray_submission_id = --a01` - - 好处:Ray 侧输出目录天然可读、可追溯 -- `status`:Ray Job 状态(PENDING/RUNNING/SUCCEEDED/FAILED/STOPPED) -- `start_time` / `end_time` -- `exit_code`(如可取) -- `failure_kind`(枚举): - - `INSUFFICIENT_RESOURCES`(匹配 “Total available GPUs … less than total desired …”) - - `USER_ERROR`(配置/数据路径错误等) - - `RUNTIME_ERROR`(代码异常) - - `UNKNOWN` - ---- - -## 5. 状态机(服务侧) - -建议最小状态集: - -- `QUEUED`:已入队,尚未进行资源判断 -- `PENDING_RESOURCES`:资源不足,等待(服务侧 pending,不提交 Ray) -- `SUBMITTING`:正在向 Ray 提交 attempt -- `SUBMITTED`:Ray 已接受 submission(拿到 `ray_submission_id`) -- `RUNNING`:Ray Job RUNNING -- `SUCCEEDED`:任务成功(终态) -- `FAILED`:任务失败(终态,除非命中“资源不足重试策略”) -- `CANCELED`:用户取消(终态) - -关键转换: -- `QUEUED -> PENDING_RESOURCES`:资源不足 -- `QUEUED/PENDING_RESOURCES -> SUBMITTING`:资源满足 -- `SUBMITTING -> SUBMITTED`:提交成功 -- `SUBMITTED -> RUNNING`:Ray 状态推进 -- `SUBMITTED/RUNNING -> SUCCEEDED|FAILED`:Ray 终态 -- `FAILED (INSUFFICIENT_RESOURCES) -> PENDING_RESOURCES`:进入延迟重试(attempt_no+1) - ---- - -## 6. 调度策略(v2.0) - -### 6.1 资源计算(对齐 verl 的“可用资源”口径) - -由于 verl 使用 `ray._private.state.available_resources_per_node()` 做“可用资源”统计, -v2.0 的 scheduler 应该尽量使用相同口径,避免: -- 我们认为够了 → 实际 verl 认为不够(仍 fail-fast) -- 我们认为不够 → 实际够了(浪费) - -策略(建议): -1) scheduler 周期性获取 per-node 可用 GPU -2) 计算 total_available_gpus = sum(node_gpu_available) -3) 任务需求 total_required_gpus = nnodes * n_gpus_per_node -4) 如果 `total_available_gpus < total_required_gpus` → `PENDING_RESOURCES` - -注意:v2.0 先只做总量判断;节点级分配(保证每个 node 恰好 n_gpus_per_node)可作为 v2.1+(资源池/标签/节点纳管)增强点。 - -### 6.2 排队与并发 - -- 默认 FIFO。 -- 并发度:允许同时跑多个任务,但必须保证资源足够。 - - 简化实现:如果任务默认都吃满 8 卡,则 scheduler 实际上一次只能跑一个。 - - 若未来支持小任务(1*1、1*4),可以自然并发。 - -### 6.3 重试策略(资源不足) - -当出现下面模式时判定为 `INSUFFICIENT_RESOURCES`: -- Ray Job `status=FAILED` -- `JobDetails.message` 或 `job logs` 中匹配: - - `Total available GPUs` 且 `less than total desired` - -处理: -- 将 task 置为 `PENDING_RESOURCES` -- `next_run_at = now + 60s`(固定间隔;v2.1 可改指数退避) -- attempt_no++ 后重提(新 submission id) - ---- - -## 7. SQLite 持久化(队列/状态/attempt) - -v2.0 引入一个**最小但可恢复的持久化层**:使用 SQLite 保存任务队列与状态,确保: -- api/scheduler 进程重启后,队列不丢; -- task/attempt 历史可追溯; -- 能实现“服务侧 pending + 延迟重试”的确定性行为。 - -### 7.1 存放位置 - -建议路径(容器内): -- `DB_PATH=/private/common/db/mvp_v2.sqlite3` - -说明: -- v2.0 默认单实例服务(单 writer),SQLite 足够。 -- 生产环境若 NFS 上的 SQLite 有锁/性能风险,v2.1+ 再演进到 Postgres/Redis;v2.0 先以“可回放/可恢复”为第一目标。 - -### 7.2 表设计(建议最小集合) - -- `tasks` - - `task_id` (PK) - - `workload` - - `state`(服务侧状态机) - - `jobspec_yaml`(原始 YAML 文本,原样落盘便于审计/复现) - - `created_at`, `updated_at` - - `next_run_at`(用于 `PENDING_RESOURCES` 的延迟重试) - - `error_summary` - - `latest_attempt_no` - -- `attempts` - - `task_id` (FK) - - `attempt_no` - - `ray_submission_id` - - `ray_status` - - `failure_kind` - - `message`(截断后的关键信息) - - `start_time`, `end_time` - -- `events`(可选,但非常利于排障) - - `id` (PK) - - `task_id` - - `ts` - - `event_type`(STATE_TRANSITION / SUBMIT / RAY_STATUS_SYNC / RETRY_SCHEDULED 等) - - `payload_json` - -### 7.3 调度循环(与 SQLite 的交互) - -scheduler 每个 tick 做三件事: -1) **挑选可运行任务**(FIFO + next_run_at): - - `state IN ('QUEUED','PENDING_RESOURCES') AND next_run_at <= now` -2) **资源判断**(对齐 verl 的可用资源口径): - - 不满足:更新 `state='PENDING_RESOURCES'`,并写入 `next_run_at=now+60s` -3) **提交 Ray Job 并追踪**: - - 提交成功:写入 `attempts` 并更新 `tasks.latest_attempt_no`、`state='SUBMITTED'` - - 周期性同步 Ray 状态:`SUBMITTED/RUNNING -> SUCCEEDED/FAILED` - - 若失败命中资源不足模式:`FAILED -> PENDING_RESOURCES` + 计划下次重试 - ---- - -## 8. 接口与验收(DoD) - -### 8.1 API 能力(最小集合) - -详见 `specs/mvp/v2.0/v2_api.md`。 - -### 8.2 验收口径(DoD) - -1) API 提交 PPO/GRPO/SFT,返回 `task_id`,并在 NFS 上创建任务目录。 -2) 当集群忙(GPU 不足)时: - - task 状态为 `PENDING_RESOURCES`(不是 FAILED) - - 一旦资源释放,任务自动变为 `SUBMITTED/RUNNING` -3) 当 race 导致触发 verl fail-fast: - - attempt 标记为 `INSUFFICIENT_RESOURCES` - - task 回到 `PENDING_RESOURCES`,并在 60s 后自动重试 -4) 通过 API 查询 task 能看到: - - 当前 state - - 最新 attempt 的 `ray_submission_id` - - attempt 历史(至少包含开始/结束/失败原因) -5) Cancel 能停止正在运行的 Ray Job(调用 Ray Jobs SDK stop) - ---- - -## 9. v2.0 交付物建议(目录) - -`specs/mvp/v2.0/`(本目录): -- `v2_plan.md`:总体设计与开发计划(本文件) -- `v2_api.md`:API 详细定义(请求/响应/字段/错误码) - -代码建议位置(后续实现时): -- `src/mvp/v2.0/` - - `py/`:API server + scheduler - - `scripts/`:启动/停止/查看状态(仍沿用 v1.1 的 compose/cluster 逻辑) diff --git a/specs/mvp/v2.5/README.md b/specs/mvp/v2.5/README.md deleted file mode 100644 index ebd49fd..0000000 --- a/specs/mvp/v2.5/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# MVP v2.5(Design)— User Management & Stateless Ray Node Pool - -本目录基于 `specs/mvp/mvp_roadmap_v2.md` 与 `specs/mvp/image/roadmap_v2.5.png` 的 v2.5 规划, -给出一份**可落地、可验证、可迭代实现**的详细方案设计文档集合。 - -v2.5 的核心变化: -- 在 v2.0 的任务队列/调度/重试基础上,引入 **User Management**(多用户隔离、目录隔离、token)。 -- 引入 **Stateless Ray Node Pool**:worker 节点/容器不再需要平台显式下发 head 地址,通过共享存储(GPFS/NFS)完成服务发现与自愈连接(watchdog)。 - -文档: -- `specs/mvp/v2.5/v2.5_design.md`:总体架构、关键机制(head IP file / watchdog / 用户隔离 / 任务流)。 -- `specs/mvp/v2.5/v2.5_api.md`:API 设计(用户、任务、队列、日志)与鉴权约定。 -- `specs/mvp/v2.5/v2.5_acceptance.md`:开发/部署/验收流程与可验证标准。 -- `specs/mvp/v2.5/v2.5_summary.md`:v2.5 已实现内容总结(本次迭代做了什么、验收结果、已知限制)。 -- `specs/mvp/v2.5/v2.5_container_design.md`:将 stateless pool 固化到单镜像(head/worker 复用 + supervisor 守护)的设计与验证流程。 diff --git a/specs/mvp/v2.5/notices.md b/specs/mvp/v2.5/notices.md deleted file mode 100644 index 6b4a2cf..0000000 --- a/specs/mvp/v2.5/notices.md +++ /dev/null @@ -1,3 +0,0 @@ -# 记录问题 -1. task 、 submission id 里加上 user name -2. 补全端到端测试用例,各种正常和异常用例,边界情况测试 \ No newline at end of file diff --git a/specs/mvp/v2.5/v2.5_acceptance.md b/specs/mvp/v2.5/v2.5_acceptance.md deleted file mode 100644 index 74e2953..0000000 --- a/specs/mvp/v2.5/v2.5_acceptance.md +++ /dev/null @@ -1,67 +0,0 @@ -# MVP v2.5 开发/部署/验收标准 - -本文件定义 v2.5 的“可验证闭环”,确保每个里程碑可验收。 - ---- - -## 1. 开发交付物(Deliverables) - -### 1.1 代码交付(建议) - -- API Server 增强:user management + task 关联 user_id + 鉴权隔离 -- SQLite schema 迁移:新增 users/tokens,tasks 增加 user_id -- Ray Head service discovery:head.json 写入与心跳刷新 -- Worker bootstrap + watchdog: - - dev:以脚本方式提供(docker compose 场景) - - prod:以容器 command/entrypoint 方式可注入 - -### 1.2 文档交付 - -- 目录结构与 GPFS 路径约定 -- API 文档(含用户与多租户隔离) -- 运维 SOP:head 重启、worker 自愈、如何排障 head.json - ---- - -## 2. 部署流程(Dev 环境可验证) - -### 2.1 启动顺序(推荐) - -1) 启动 head(包含 API server + Ray head) -2) head 写入 `/private/ray/discovery//head.json` -3) 启动若干 worker(无须指定 head 地址) -4) worker 自动读取 head.json 并加入集群 -5) 通过 API 创建用户并获取 token -6) 使用 user token 提交 PPO/GRPO/SFT - ---- - -## 3. 验收标准(Acceptance Criteria) - -### 3.1 Stateless Ray Node Pool - -- A1:在 worker 启动时不传 head 地址,worker 能在 `T<=60s` 内加入集群(ray status 可见) -- A2:head 容器重启(IP 变化或 Ray 重启)后: - - head.json 更新 - - worker watchdog 在 `T<=60s` 内自动重连 -- A3:head 设置 `--num-gpus=0 --num-cpus=0`,训练 driver 不会跑到 head(可通过 Ray dashboard/日志验证) - -### 3.2 User Management - -- U1:admin 可创建用户并签发 token(token 仅返回一次) -- U2:用户 A 提交的 task,用户 B 无法查询/取消/获取日志(API 返回 404 或 403,按设计约定) -- U3:仅隔离 jobs 输出:任务输出落在 `/private/users//jobs//...`,不同用户互不覆盖 -- U4:训练输入(verl 代码、HF cache、datasets)统一使用 `/private/common/...`(v2.5 不做输入隔离) - -### 3.3 Task Flow(继承 v2.0) - -- T1:PPO/GRPO/SFT 三种 workload 都能成功提交并跑通(dev 规模可用 epoch=1/steps=10) -- T2:资源不足时任务不会“直接失败不可恢复”,而是进入 `PENDING_RESOURCES` 并按间隔重试(与 v2.0 同逻辑) - ---- - -## 4. 回归用例(最小集合) - -1) 创建用户 alice/bob,分别提交 sft,验证隔离与输出目录 -2) 启动 head + 2 workers,提交 ppo/grpo,验证 driver 落 worker -3) 重启 head(或修改 head.json 指向新 IP),验证 worker watchdog 自动重连 diff --git a/specs/mvp/v2.5/v2.5_api.md b/specs/mvp/v2.5/v2.5_api.md deleted file mode 100644 index 2a37650..0000000 --- a/specs/mvp/v2.5/v2.5_api.md +++ /dev/null @@ -1,109 +0,0 @@ -# MVP v2.5 API 设计(User + Task + Queue) - -v2.5 在 v2.0 API 基础上,新增 **User Management** 与多租户隔离。 - -约束: -- 仍使用内部 token(API key); -- 不引入外部 IAM; -- TaskSpec 仍为 YAML(沿用现有结构化字段)。 - ---- - -## 1. Auth - -Header: -- `Authorization: Bearer ` - -服务端行为: -- 将 `api_token` 映射到 `user_id` -- 之后的 task 操作默认仅作用于该 `user_id` - -Admin token(可选): -- 支持额外配置 `MVP_ADMIN_TOKEN`(或 user.role=admin) -- admin 可跨用户查询/取消(用于运维)。 - ---- - -## 2. User Management - -### 2.1 创建用户(admin) - -`POST /api/v2/users` - -Request(JSON): -```json -{"user_id":"alice","display_name":"Alice"} -``` - -Response: -```json -{"user_id":"alice","state":"ACTIVE"} -``` - -### 2.2 为用户签发 token(admin) - -`POST /api/v2/users/{user_id}/tokens` - -Response(只返回一次明文 token): -```json -{"user_id":"alice","token":"mvp_u_..."} -``` - -### 2.3 禁用用户(admin) - -`POST /api/v2/users/{user_id}:disable` - ---- - -## 3. Task Management(多租户) - -### 3.1 提交任务 - -`POST /api/v2/tasks` - -Body: -- `Content-Type: application/yaml` -- raw TaskSpec YAML(训练语义字段;不含 user_id) - -Response: -```json -{"task_id":"mvp25-ppo-20251225-170001-2a3f","state":"QUEUED"} -``` - -服务端 side effects: -- 记录 tasks.user_id(由 token 得到) -- 计算输出目录:`/private/users//jobs//...` - -### 3.2 查询任务(仅本人) - -`GET /api/v2/tasks/{task_id}` - -若 task 不属于当前 user: -- 返回 `404`(避免泄露存在性) - -### 3.3 取消任务(仅本人) - -`POST /api/v2/tasks/{task_id}:cancel` - ---- - -## 4. Queue/Debug - -### 4.1 查看队列(本人视角) - -`GET /api/v2/queue` - -返回该 user 的 pending/running 列表。 - -### 4.2 管理员查看全局队列(admin) - -`GET /api/v2/admin/queue` - ---- - -## 5. Logs - -`GET /api/v2/tasks/{task_id}/logs?attempt=latest&tail=2000` - -行为与 v2.0 一致:透传 Ray Job logs tail。 - diff --git a/specs/mvp/v2.5/v2.5_container_design.md b/specs/mvp/v2.5/v2.5_container_design.md deleted file mode 100644 index 653ea76..0000000 --- a/specs/mvp/v2.5/v2.5_container_design.md +++ /dev/null @@ -1,202 +0,0 @@ -# MVP v2.5 — Stateless Ray Node Pool 容器固化设计 - -目标:把 v2.5 的 **stateless pool(head discovery + worker watchdog)** 能力固化到一个可复用镜像中,避免依赖宿主机脚本在容器内 `docker exec` 启动/守护进程。**同一个镜像同时供 head/worker 复用**,通过环境变量区分角色。 -约束:**API server 代码与镜像解耦**,短期仍按现状“宿主机代码挂载到 head 容器,在 head 容器内启动 API”,不把 API 代码打进本镜像。 - ---- - -## 1. 背景(现状与痛点) - -当前 `src/mvp/docker-compose.yaml` 里 head/worker 都基于 `verlai/verl:sgl055.latest`,容器启动后 `command: sleep infinity`,再由宿主机脚本完成: -- head:`ray start --head ...` + `head_publisher`(写 `head.json`) -- worker:`worker_watchdog`(读取 `head.json`,自动加入/重连 ray 集群) - -现状问题: -- 启动流程依赖宿主脚本 `docker exec`,易受权限/路径/人为操作影响; -- “守护”目前是 bash while-loop,出现异常时排障成本高; -- 未来生产环境容器可能由算力平台拉起,我们只能 SSH 纳管,更需要把“自启动 + 自愈”放到容器内部。 - ---- - -## 2. v2.5 容器固化目标与非目标 - -### 2.1 目标 -- **一个镜像复用**:head/worker 统一镜像,通过 `ARGUS_ROLE=head|worker` 区分。 -- **supervisor 守护**:无论 head/worker,都使用 `supervisord` 守护关键进程: - - watchdog 崩溃 → supervisor 自动重启 watchdog - - ray 节点崩溃 → watchdog/或 supervisor 触发自动恢复(见 3.2 进程模型) -- **与共享存储对齐**:容器内统一挂载根路径 `/private`;discovery 文件写到共享存储。 -- **最小内置代码**:镜像只内置 stateless pool 相关 python 脚本(discovery/publisher/watchdog/entrypoint),不把 API 服务代码打进镜像。 - - **远端构建**:镜像构建必须在开发/运行机器(例如 `argus@h1`)上完成,本机不要求具备 `verlai/verl:*` 基础镜像。 - -### 2.2 非目标(本迭代不做) -- 不把 API server 打包进本镜像(后续可做单独 `argus-api` 镜像)。 -- 不改变 v2.5 TaskSpec 约束(仍使用 `/private/common/...` 公共资源;用户隔离只隔离 jobs)。 -- 不在本迭代引入 K8s/operator/autoscaler;只固化容器自启动/自愈。 - ---- - -## 3. 设计方案 - -### 3.1 单镜像架构概览 - -新增一个镜像(示例名): -- `argus/argus-ray-node:v2.5` - -该镜像: -- `FROM verlai/verl:sgl055.latest`(通过 build-arg 可切换 base) -- 内置: - - `argus_raypool`(或复用现有 `argus.ray.*` 子集)脚本: - - `discovery.py`:head record 读写(head.json) - - `head_publisher.py`:head 写入 head.json(带 TTL/刷新) - - `worker_watchdog.py`:worker 读取 head.json,自动加入/重连 - - (可选)`head_watchdog.py`:把 “ray head + publisher” 组装成一个可恢复的 watchdog - - `/usr/local/bin/argus-entrypoint.sh`:根据 role 生成 supervisor 配置并启动 supervisor - - supervisor 配置模板(或运行时生成) - -### 3.2 进程模型(确保“ray 崩/ watchdog 崩都能恢复”) - -用户新增要求:head/worker 均要 supervisor 守护 watchdog;ray 节点崩溃或 watchdog 崩溃都要自动恢复。 - -推荐进程组织(避免 “ray start” 后台化导致 supervisor 无法感知): - -#### A) Head 容器(ARGUS_ROLE=head) -由 supervisor 启动 **两个 program**: -1) `argus_head_watchdog`(推荐实现为 python 或 bash,内部用 `ray start --head --block` 前台运行) - - 关键点:`ray start --head --block` 让 Ray 进程前台阻塞,watchdog 作为父进程能感知退出码 - - ray 崩 → `ray start --block` 返回 → watchdog 退出非 0 → supervisor 重启 watchdog → ray 自动重启 -2) `argus_head_publisher` - - 定期刷新 `head.json`(TTL/refresh) - - publisher 崩 → supervisor 自动重启 - -> 备选:把 publisher 逻辑合并进 `argus_head_watchdog`(一个进程同时跑 ray + publisher 线程),减少 supervisor program 数量;但拆分更易观测与定位问题。 - -#### B) Worker 容器(ARGUS_ROLE=worker) -由 supervisor 启动 **一个 program**: -1) `argus_worker_watchdog` - - 轮询读取 `head.json`,并以 `ray start --address=:6379 --block` 方式加入集群 - - 只要 ray 进程退出(ray 崩/被 stop),`--block` 结束,watchdog 进入下一轮重连/重启 - - watchdog 自己异常退出 → supervisor 自动重启 watchdog - -> 注意:当前仓库里的 `worker_watchdog.py` 是 “`ray start` 非 block + 仅在 head addr 变化时重启”。容器固化建议升级为 “`--block` + 监测 ray 退出” 模式,否则 supervisor 很难准确感知 ray 的生命周期。 - -### 3.3 配置与环境变量(Role 驱动) - -镜像入口只依赖环境变量,不依赖宿主脚本参数。 - -建议环境变量清单(含默认值): -- `ARGUS_ROLE`:`head` / `worker`(必填) -- `ARGUS_SHARED_ROOT`:默认 `/private` -- `ARGUS_CLUSTER_NAME`:默认 `argus-ray` -- `ARGUS_HEAD_IP_FILE`:默认 `${ARGUS_SHARED_ROOT}/ray/discovery/${ARGUS_CLUSTER_NAME}/head.json` -- `ARGUS_RAY_PORT`:默认 `6379` -- `ARGUS_DASHBOARD_PORT`:默认 `8265`(head) -- `ARGUS_TTL_S`:默认 `60`(head publisher) -- `ARGUS_REFRESH_S`:默认 `10`(head publisher) -- `ARGUS_POLL_S`:默认 `5`(worker watchdog) -- `ARGUS_NODE_IP`:默认空;若空则 entrypoint 自动探测容器 IP -- `ARGUS_WORKER_RESOURCES_KV`:默认 `worker_node=100`(用于 driver 强制落 worker 的自定义资源) -- `ARGUS_RAY_EXTRA_ARGS`:可选,传递额外 `ray start` 参数 -- `ARGUS_LOG_DIR`:默认 `${ARGUS_SHARED_ROOT}/common/logs`(落到共享目录便于排障) - -### 3.4 Dockerfile / entrypoint / supervisor 设计 - -#### Dockerfile(建议路径) -在仓库新增(后续实现时): -- `src/mvp/images/argus-ray-node/Dockerfile` -- `src/mvp/images/argus-ray-node/entrypoint.sh` -- `src/mvp/images/argus-ray-node/supervisord.conf.tmpl`(可选) -- `src/mvp/images/argus-ray-node/py/argus_raypool/*.py`(仅 stateless pool 子集) - -Dockerfile 关键动作: -- `FROM verlai/verl:sgl055.latest`(可 `ARG BASE_IMAGE=...`) -- 安装 supervisor: - - Debian/Ubuntu 基底:`apt-get update && apt-get install -y supervisor` - - 设定 `CMD ["supervisord","-n","-c","/etc/supervisor/supervisord.conf"]` -- 拷贝 python 脚本到 `/opt/argus/raypool` 并设置 `PYTHONPATH=/opt/argus` -- 拷贝 entrypoint 到 `/usr/local/bin/argus-entrypoint.sh` -- `ENTRYPOINT ["/usr/local/bin/argus-entrypoint.sh"]` - -entrypoint.sh 逻辑: -- 探测容器 IP(如 `hostname -i` 或 `ip route get 1.1.1.1`) -- 根据 `ARGUS_ROLE` 生成 supervisor 配置: - - head:启动 `head_watchdog` + `head_publisher` - - worker:启动 `worker_watchdog` -- 配置 supervisor: - - `autorestart=true` - - `startretries` 合理配置 - - stdout/stderr 指向 `${ARGUS_LOG_DIR}/...` 或直接 stdout(便于 `docker logs`) - -### 3.5 与 API server 的关系(保持解耦) -API server 仍按现状(短期方案): -- **代码存放在宿主机**,通过 volume mount 挂载到 head 容器(例如 `/workspace/mvp`)。 -- **在 head 容器内启动 API**(例如用脚本 `docker exec argus-ray-head ... python3 /workspace/mvp/py/server.py`)。 -- 关键点:即使 API 进程跑在 head 容器里,也仍视作“独立于 ray node 镜像的业务代码”,后续可独立演进为单独的 `argus-api` 镜像。 -- 只要 API 能访问 Ray job server(通常 `http://127.0.0.1:8265` 在 head 容器视角)即可。 - -未来(非本迭代)可将 API server 单独做 `argus-api` 镜像,按相同 `/private` 共享目录运行。 - ---- - -## 4. docker-compose 调整建议(后续实现) - -当前 compose 的变化点(概念上): -- `image: verlai/verl:sgl055.latest` → `image: argus/argus-ray-node:v2.5` -- `command: sleep infinity` 移除(镜像自带 entrypoint) -- head service 增加: - - `ARGUS_ROLE=head` - - 暴露 dashboard 端口保持 `8265:8265` -- worker service 增加: - - `ARGUS_ROLE=worker` - - `ARGUS_WORKER_RESOURCES_KV=worker_node=100` -- volumes 仍需要: - - `../../shared:/private`(共享存储) - - `../../verl:/workspace/verl`(verl 代码/依赖按现状) - ---- - -## 5. 验证与回归流程(落地后怎么验收) - -### 5.1 构建镜像 -1) **在远端 `argus@h1` 构建**(本机不要求具备基础镜像): - - `cd /home2/argus/infra/mvp/src/mvp` - - `docker build -t argus/argus-ray-node:v2.5 -f images/argus-ray-node/Dockerfile .` -2) 也可以使用 compose build(推荐,和实际运行一致): - - `docker compose -f docker-compose.yaml build --no-cache` - -### 5.2 基础连通性(stateless pool 验证) -1) `docker compose up -d` -2) 验证 head 写入: - - 共享目录存在 `head.json`:`${ARGUS_SHARED_ROOT}/ray/discovery/${ARGUS_CLUSTER_NAME}/head.json` -3) 验证 worker 自动加入: - - 在 head 容器内 `ray status` 能看到 worker 节点加入 - - Dashboard Nodes 页面能看到 head + worker - -### 5.3 故障注入(supervisor 自愈验证) -1) watchdog 崩溃: - - `pkill -f worker_watchdog`(或 kill 对应 PID) - - 期望:supervisor 自动拉起 watchdog;worker 最终重新加入集群 -2) ray 节点崩溃(worker): - - `ray stop --force` 或 kill raylet - - 期望:watchdog 重新执行 `ray start ... --block`,worker 恢复 -3) ray 节点崩溃(head): - - kill head ray 前台进程(由 watchdog 启动) - - 期望:supervisor 重启 head_watchdog;head 恢复并重写 head.json;workers 自动重连 - -### 5.4 端到端任务回归(与 v2.5 API 协作) -沿用现有 v2.5 E2E: -- `src/mvp/scripts/run_all_v25_api.sh` -- `src/mvp/scripts/run_e2e_v25_cases.sh` - -验收标准: -- PPO/GRPO/SFT 均能在 worker 上运行,head 不跑训练 -- API 的 task_id / submission_id 正常携带用户名 -- 资源不足可转 `PENDING_RESOURCES` 并按周期重试 - ---- - -## 6. 风险点与对策 - -- **ray start 后台化**:如果继续后台启动,supervisor 不易感知 ray 崩溃。对策:使用 `ray start --block`(推荐)。 -- **IP 探测不稳定**:不同环境(compose/平台)容器 IP 获取方式不同。对策:entrypoint 做多策略探测并允许 `ARGUS_NODE_IP` 显式覆盖。 -- **日志可观测性**:建议同时支持写到 `/private/common/logs`(共享)以及 stdout(`docker logs`)。 diff --git a/specs/mvp/v2.5/v2.5_design.md b/specs/mvp/v2.5/v2.5_design.md deleted file mode 100644 index 8ae30ad..0000000 --- a/specs/mvp/v2.5/v2.5_design.md +++ /dev/null @@ -1,255 +0,0 @@ -# MVP v2.5 详细设计方案(User Management + Stateless Ray Node Pool) - -本文目标:把 `mvp_roadmap_v2.md` 中 v2.5 的思路落到**可工程化实现**的设计层,包括: -- API Server 内新增 user management; -- Ray node pool 变为无状态(worker 自发现 head、自动加入、watchdog 自愈); -- 仍保持 v2.0 的“任务管理层”语义:Task/Attempt、队列、资源判断、Ray Job 提交与状态同步; -- 所有共享数据/状态统一落在 GPFS(dev 环境可先用 NFS),容器内路径统一为 `/private/`。 - -> 术语说明:文中“GPFS”代表生产共享存储;dev 环境可用 NFS,但容器内仍以 `/private/` 访问。 - ---- - -## 1. 目标与非目标 - -### 1.1 v2.5 目标(Must) - -1) **User Management(最小多租户)** -- 支持创建/禁用用户; -- 为每个用户签发内部 token(API key),用于认证与隔离; -- 用户隔离(v2.5 先做最小闭环,仅隔离 **jobs 输出** 与 API 可见性): - - 用户只能看到/操作自己的 Task; - - 训练输出(job root、checkpoints、日志归档等)按 user 目录落盘; - - 训练输入(verl 代码、HF cache、datasets)统一使用 `common/`(v2.5 不支持用户自定义代码/模型/数据集隔离)。 - -2) **Stateless Ray Worker Node Pool** -- worker 容器启动时无需被平台告知 head 地址; -- worker 通过 GPFS 读取 **Head IP File** 自动连接 Ray head; -- worker 内部 watchdog 监控 head 地址变化,发生变化时自动 `ray stop` + `ray start` 重连; -- worker 尽量不依赖本地持久化状态(宕机/替换后可无感重建)。 - -3) **保持 v2.0 的 Task 管理行为** -- Task/Attempt 模型不变(或向后兼容扩展); -- 对齐 verl 的 fail-fast 行为:资源不足时服务侧 pending + 重试; -- Ray Job 提交仍通过 Ray Python SDK(JobSubmissionClient)。 - -### 1.2 v2.5 非目标(Not Now) - -- 完整 WebUI(留到 v3.0)。 -- 公平调度/配额/优先级(留到 v3.x)。 -- 完整生产级 IAM(留到 v4+),v2.5 仅内部 token。 -- K8s 原生编排(本阶段不要求,但设计需能适配“算力平台拉起容器,只能 ssh 进去纳管”的模式)。 - ---- - -## 2. 总体架构(对应 roadmap v2.5) - -### 2.1 组件划分 - -**控制面(Control Plane)** -- **API Server** - - user management - - task management(队列/调度/重试/状态聚合) - - Ray Job Tool(Ray Client) - - VerlTaskSpec(TaskSpec YAML,沿用 v2.0/v2.1 格式) - - 与 Ray head 在同一台/同一容器是推荐形态(便于访问 dashboard / job server) -- **Ray Head(有状态)** - - 启动后把 head 地址写入 GPFS 的 Head IP File,用于 worker 服务发现 - -**数据面(Data Plane)** -- **Ray Workers(无状态节点池)** - - stateless bootstrap:从 GPFS 读取 head 地址自动加入集群 - - watchdog:持续 watch head 地址文件变化并自愈重连 - -**共享存储(GPFS)** -- 统一数据路径:数据、模型 cache、代码、任务输出、以及 head 服务发现文件。 - -### 2.2 v2.5 的控制反转(IoC) - -与 v2.0/手工集群的关键差异: -- v2.0:平台脚本/运维显式启动 worker 并指定 `--address=`。 -- v2.5:worker 自己从 GPFS 读取 `head_ip_file`,无需平台维持 worker 列表与 SSH 连接池。 - ---- - -## 3. GPFS 目录结构(容器内 `/private`) - -建议在 v2.5 固化以下目录(与现有 v2.0 兼容扩展): - -``` -/private/ - ray/ - discovery/ - / - head.json # Head IP File(服务发现) - head.json.lock # 可选:写入锁(v2.5 可先不实现) - users/ - / - jobs/ # /private/users//jobs//* - outputs/ # 训练输出聚合(按需要) - common/ - code/ # 平台/公共代码快照(verl code snapshot 等) - datasets/ # 公共数据集 - hf/ # 公共 HF cache(dev 复用) - db/ # sqlite - logs/ # API 日志、平台日志 -``` - -说明: -- `common/`:平台默认目录(v2.5 先默认所有用户可写;后续再加 ACL/只读)。 -- `users//...`:用户隔离主边界(最小多租户的关键)。 - ---- - -## 4. Head IP File(服务发现)设计 - -### 4.1 文件路径 - -- `head_ip_file = /private/ray/discovery//head.json` -- ``:由配置指定(例如 `argus-ray`),允许同一 GPFS 上存在多个环境/集群。 - -### 4.2 文件内容(JSON) - -建议采用 JSON(易扩展): - -```json -{ - "cluster_name": "argus-ray", - "head_ip": "10.0.0.12", - "gcs_port": 6379, - "dashboard_port": 8265, - "job_server_url": "http://10.0.0.12:8265", - "updated_at": "2025-12-25T17:00:00Z", - "expires_at": "2025-12-25T17:01:00Z" -} -``` - -关键点: -- `updated_at`:便于排障与可观测; -- `expires_at`:避免 worker 读取到“陈旧 head 地址”后无限重连; -- `job_server_url`:对外可直接用于 Ray Job Tool 配置(便于无脑接入)。 - -### 4.3 写入策略(原子更新) - -Head 写入时必须保证 worker 读取不会读到半文件: -- 写临时文件 `head.json.tmp`; -- `fsync`(可选); -- `rename(head.json.tmp -> head.json)`(原子替换)。 - -### 4.4 心跳与 TTL - -Head 进程需周期性刷新 `head.json`: -- 建议 `ttl_s=60`,刷新周期 `refresh_s=10`; -- 若 head 进程异常退出,worker 读取到过期文件可进入“等待模式”而非无限重连。 - ---- - -## 5. Stateless Worker Bootstrap + Watchdog - -### 5.1 启动序列(worker 容器内) - -1) 启动脚本读取 `head.json`: - - 若文件不存在:sleep + 重试(直到存在) - - 若存在但 `expires_at` 已过期:sleep + 重试(直到变为新鲜) -2) 解析 `head_ip:gcs_port` 并执行: - - `ray stop --force || true` - - `ray start --address=: --resources='{"worker_node": 100, ...}' ...` -3) 启动 watchdog 进程(同容器): - - 轮询/监听 `head.json` 的内容变化 - - 一旦 `head_ip` 或 `gcs_port` 改变,触发 `ray stop` + `ray start` 重连 - -### 5.2 Watchdog 策略(最小可用) - -v2.5 推荐“简单且稳”的实现: -- polling 间隔 `watch_s=5`; -- 对比 `head.json` 的 `updated_at` 或 hash; -- 若发现变更:执行重连; -- 若连续多次重连失败:指数退避(v2.5 可先固定退避,v2.6 再增强)。 - -### 5.3 资源标签(driver 强制落 worker) - -继续沿用 v2.0 的思路: -- worker 启动时 `--resources='{"worker_node": 100}'` -- head 不包含 `worker_node` 资源 -- Ray job submit 时设置 entrypoint_resources:`{"worker_node": 1}` - -### 5.4 GPU/CPU 的“无状态”约束 - -- worker 是否有 GPU 由底层算力平台决定(生产上平台会为容器挂载 GPU); -- worker 启动脚本不应硬编码 GPU 编号,只依赖 `NVIDIA_VISIBLE_DEVICES`/平台注入; -- head 推荐 `--num-gpus=0 --num-cpus=0`,避免训练调度到 head。 - ---- - -## 6. User Management 设计(最小多租户) - -### 6.1 数据模型(SQLite) - -新增两张表(示意): -- `users` - - `user_id`(PK) - - `display_name` - - `state`(ACTIVE/DISABLED) - - `created_at` -- `api_tokens` - - `token_hash`(PK) - - `user_id`(FK) - - `created_at` - - `last_used_at` - -并在 `tasks` 表增加: -- `user_id`(FK) - -### 6.2 鉴权策略 - -内部 token 模式: -- `Authorization: Bearer ` -- 服务端将 token 映射到 `user_id` -- 后续所有 task 查询/取消/日志默认 scope 到该 `user_id` - -管理员能力(v2.5 最小实现): -- 额外配置一个 admin token(或把特定 user 标记为 admin) -- admin 可 list all users/tasks(用于运维排障)。 - -### 6.3 用户目录隔离(路径约束) - -核心原则(v2.5 版): -- **输出**:必须落在 `/private/users//jobs/...`(服务端统一计算,不允许用户任意指定输出根) -- **输入**:统一使用 `/private/common/...`(v2.5 不支持用户自定义 verl 代码、也不做 hf/datasets 的用户隔离) - -服务端处理策略(最小可用): -- 解析 TaskSpec 后,对输入路径字段做白名单前缀校验(必须是 `/private/common/...`;拒绝 `../` 与越界路径); -- 输出目录统一由服务端计算:`job_root = /private/users//jobs//`。 - ---- - -## 7. TaskSpec(VerlTaskSpec YAML)在 v2.5 的扩展点 - -v2.5 **不扩展 TaskSpec**:保持与 v2.0/v2.1 的 YAML 结构化字段与语义一致。 - -v2.5 的“用户语义”仅体现在服务端的补齐/约束: -- user_id 由 token 推导(用户不需要在 YAML 里写 user_id); -- 服务端派生 `ray_submission_id`(由 task_id/attempt 派生); -- 服务端统一计算输出目录 `job_root=/private/users//jobs//...`; -- v2.5 不支持用户自定义 verl 代码路径(因此 runtime_env 不需要注入用户 code 目录)。 - ---- - -## 8. 迁移与兼容性 - -v2.5 设计需满足: -- 现有 v2.0 的“手工启动 worker”仍可运行(作为 dev fallback); -- 在不改镜像的前提下,worker watchdog 可以以“容器启动命令/entrypoint”方式注入(dev 用 scripts;生产由算力平台指定 command)。 - ---- - -## 9. 风险与对策(v2.5) - -1) **GPFS 上 head.json 一致性/延迟** -- 对策:原子 rename + TTL;watchdog polling。 - -2) **Ray head 重启后 job server URL 变化** -- 对策:head.json 内写入 `job_server_url`,Ray Job Tool 可读取该文件更新 address(v2.6 可做动态 reload)。 - -3) **Worker 重连期间任务波动** -- 对策:服务侧调度器对齐 verl 的资源 fail-fast;任务失败可归因并排队重试。 diff --git a/specs/mvp/v2.5/v2.5_dev_plan.md b/specs/mvp/v2.5/v2.5_dev_plan.md deleted file mode 100644 index ce2bad6..0000000 --- a/specs/mvp/v2.5/v2.5_dev_plan.md +++ /dev/null @@ -1,229 +0,0 @@ -# MVP v2.5 开发计划(TDD 驱动) - -本文是 v2.5 的**工程化开发计划**,强调“先写测试,再写实现”(TDD),并将每个里程碑拆成**可独立验收**的小闭环。 - -输入依据: -- 路线图:`specs/mvp/mvp_roadmap_v2.md` -- v2.5 设计:`specs/mvp/v2.5/v2.5_design.md` -- v2.5 API 草案:`specs/mvp/v2.5/v2.5_api.md` -- v2.5 验收:`specs/mvp/v2.5/v2.5_acceptance.md` - -v2.5 约束(已确认): -- **不扩展 TaskSpec**:沿用 v2.0/v2.1 的 YAML 结构化字段与语义。 -- **不支持自定义 reward function / 不支持用户自定义 verl 代码**。 -- 训练输入(verl 代码、HF cache、datasets)统一使用 `/private/common/...`。 -- 多用户隔离 v2.5 **先只隔离 jobs 输出目录**:`/private/users//jobs//...`。 - ---- - -## 0. TDD 规范(所有功能都遵循) - -### 0.1 测试分层 - -1) **单元测试(fast)** -- 纯 Python 逻辑:DB、鉴权、ID、路径派生、head.json 解析/TTL、watchdog 决策逻辑。 -- 目标:不依赖真实 Ray、不依赖 docker、不依赖网络。 - -2) **组件测试(中等)** -- FastAPI 路由:使用 `fastapi.testclient.TestClient`(现有 v2.0 已采用)。 -- 目标:验证 auth/权限隔离、API 行为、状态机。 - -3) **端到端(慢/手工或脚本)** -- 在 `argus@h1` 上通过 scripts/compose 跑一次“head publish → worker auto-connect → API submit”闭环。 -- 目标:验证无状态 worker + watchdog 的真实行为。 - -### 0.2 测试约定 - -- 测试目录:`src/mvp/py/tests/` -- 新增功能必须先补齐测试用例,并让其在未实现时失败(红)。 -- 实现最小改动让测试变绿(绿)。 -- 重构/去重复(重构)。 - -> 注:现有测试通过 `src/mvp/py/tests/conftest.py` 注入 ray stub,确保单测不依赖真实 ray 包;v2.5 新增模块也应复用此模式。 - ---- - -## 1. 里程碑拆分(v2.5 = 4 个可验证闭环) - -### M1:User 表/Token 表 + 基础鉴权(不影响现有内部 token 兼容) - -**目标** -- 引入 user/token 的持久化与鉴权映射(token → user_id)。 -- 兼容现有 `Authorization: Bearer ` 的“单租户模式”,避免一次性破坏 v2.0 用法: - - v2.5 可以先支持两种 token 模式: - - legacy:环境变量 `MVP_INTERNAL_TOKEN`(全局单租户); - - user token:DB 内签发 token(多用户)。 -- admin 能创建用户、签发 token、禁用用户。 - -**TDD 用例(先写测试)** - -单测: -- `test_user_db_create_disable()` - - 创建用户 ACTIVE;禁用后状态变为 DISABLED;重复创建返回冲突或幂等(按最终约定)。 -- `test_token_hashing()` - - 签发 token 时 DB 中只保存 hash,不保存明文。 - -API 测试(TestClient): -- `test_admin_create_user_and_issue_token()` - - admin token 可创建用户并签发 token(明文 token 只返回一次)。 -- `test_disabled_user_token_rejected()` - - 用户被禁用后,使用旧 token 调用 API 返回 401/403。 - -**实现落点(建议模块)** -- `argus.service.auth`:token 校验与 user_id 解析(兼容 legacy 模式) -- `argus.service.db`:新增 `users`、`api_tokens` 表与 CRUD -- `argus.service.app`:新增 user 管理 endpoints(admin scope) -- `configs/dev.yaml`:补充 admin token/env 相关配置(保持 YAML 风格) - -**验收点** -- `v2.5_acceptance.md`:U1 可通过自动化 API 测试覆盖。 - ---- - -### M2:Task 绑定 user_id + API 可见性隔离(仍不改 TaskSpec) - -**目标** -- 提交 task 时由 token 推导 `user_id`,写入 `tasks.user_id`。 -- task 查询/取消/日志默认只允许 owner;他人访问返回 404(避免泄露存在性)。 -- queue 默认只返回当前用户队列;admin 可查询全局队列(可选)。 - -**TDD 用例(先写测试)** - -单测: -- `test_tasks_table_has_user_id()`:创建任务必须落 `user_id`,且 `list_queue(user_id=...)` 只返回该用户任务。 - -API 测试: -- `test_task_visibility_isolated()` - - user A 创建 task;user B 查询 `/api/v2/tasks/{id}` 返回 404; - - user B cancel/logs 也返回 404。 -- `test_queue_isolated()` - - A/B 各自创建 task;`GET /api/v2/queue` 只看到自己的。 - -**实现落点** -- `argus.service.app`:为 task endpoints 增加 user scope -- `argus.service.db`:tasks 表增加 user_id 字段、索引、按 user 过滤的查询方法 -- `argus.service.scheduler`:pick_next_runnable_task 等仍按“全局 FIFO”或“按 user FIFO” - - v2.5 先保持“全局 FIFO”最简单(但 API queue 视角是按 user 过滤)。 - -**验收点** -- `v2.5_acceptance.md`:U2 可通过 API 测试覆盖。 - ---- - -### M3:Jobs 输出目录按 user 隔离(只改输出,不改输入) - -**目标** -- Ray Job 的 job_root 目录由服务端统一计算到: - - `/private/users//jobs//...` -- TaskSpec 内与输入相关的路径字段必须是 `/private/common/...`(v2.5 输入统一 common)。 -- 任何用户无法通过 TaskSpec 指定输出写到非 user jobs 目录(避免越权写)。 - -**TDD 用例(先写测试)** - -单测: -- `test_job_root_derivation_per_user()` - - 给定 user_id 与 ray_submission_id,派生 job_root 固定且正确。 -- `test_reject_non_common_inputs()` - - TaskSpec 中 train_file / val_file / code_path / hf 路径等若不以 `/private/common/` 开头则拒绝(HTTP 400)。 - -API 测试: -- `test_job_dir_written_under_user_jobs()` - - 提交 task 后,在 DB 或 submit payload 中能看到 job_root 在 user jobs 下(可通过 mock RayJobTool.submit 捕获 spec)。 - -**实现落点(建议最小侵入)** -- 在 service 层派生 `job_root` 并注入到 RayJobTool/builders(而不是让用户从 TaskSpec 指定)。 -- RayJobTool `_job_dir()` 改为接收“job_root 生成器”或直接接收 `job_root` 参数(由服务层提供)。 - - 目标:保持 RayJobTool 的职责清晰:提交 Ray job;路径策略由 service 决定。 - -**验收点** -- `v2.5_acceptance.md`:U3/U4 可通过 API/单测覆盖。 - ---- - -### M4:Stateless Ray Node Pool(head.json + worker watchdog)+ 端到端脚本验证 - -**目标** -- head 启动后持续写入 `/private/ray/discovery//head.json`(包含 TTL)。 -- worker 容器内运行 watchdog(或启动脚本 + watchdog),无需平台显式传 head 地址: - - 读取 head.json(存在且未过期)→ `ray start --address=:` - - head.json 变化 → `ray stop` + `ray start` 重连 -- 在 dev 环境(docker compose)提供一键脚本复现(e2e)。 - -**TDD 用例(先写测试)** - -单测(不跑真实 ray): -- `test_head_json_read_validate_ttl()` - - 文件不存在/过期 → 返回“不可用” - - 未过期 → 返回 head 地址 -- `test_watchdog_decision_on_change()` - - head_ip 变化 → 触发重连动作 - - only updated_at 变化(地址不变)→ 不重连(或按策略重连,需确定) - -组件/脚本级测试(可选): -- 如果 watchdog 用 Python 实现,可对“执行命令”层做 stub(不真正跑 `ray start`),只验证会调用什么命令。 - -端到端脚本(手工/慢): -- 提供脚本 `scripts/run_all_v25_stateless.sh`(命名示例): - 1) 起 head(Ray head + API) - 2) 启动 head publisher(写 head.json) - 3) 起 2 个 worker(每个 4 GPU),worker 只跑 watchdog,不传 head 地址 - 4) `ray status` 显示 1 head + 2 worker 且 GPU=8 - 5) 通过 API 创建用户/签发 token,提交 PPO/GRPO/SFT - 6) 重启 head(或更新 head.json 指向新地址)验证 worker 自动重连 - -**实现落点(建议实现策略)** - -为了可测试性(TDD),推荐把“读 head.json/判定 TTL/生成 ray start 命令”做成 Python 模块: -- `argus.ray.discovery`:read/write head.json(原子写、TTL) -- `argus.ray.worker_watchdog`:watch loop(polling + change detection),执行命令可注入(便于单测 stub) - -脚本层保持薄: -- `scripts/` 负责 docker exec / compose 编排与进程守护; -- watchdog 进程由容器内 python 模块运行(更可测、更易移植到生产平台的 entrypoint/command)。 - -**验收点** -- `v2.5_acceptance.md`:A1/A2/A3 主要通过 e2e 脚本 + dashboard/日志验证。 - ---- - -## 2. 回归策略(确保 v2.0 不被破坏) - -在 v2.5 过程中保留并持续回归以下用例(至少单测覆盖): -- 旧的内部 token 模式仍可访问 `GET /api/v2/queue` 与提交 task(若决定保留兼容)。 -- scheduler 的“资源不足 → PENDING_RESOURCES → 延迟重试”行为不变(现有 `test_scheduler.py` 覆盖)。 -- `ray entrypoint_resources` 强制 driver 落 worker(继续使用 `worker_node` 自定义资源)。 - ---- - -## 3. 交付清单(代码/脚本/文档) - -### 3.1 代码 -- user/tokens:DB schema + auth + API endpoints -- tasks:绑定 user_id + 权限隔离 -- job_root:按 user jobs 输出目录派生(输入仍 common) -- discovery/watchdog:head.json + worker 自愈 - -### 3.2 scripts(dev e2e) -- head:启动 Ray head + head publisher -- workers:以无状态方式启动(不传 head addr)+ watchdog -- `run_all`:一键跑通(含 API submit + 查询 + cancel + 观察队列) - -### 3.3 文档 -- 更新 `specs/mvp/v2.5/*`(设计/API/验收/开发计划) -- 补充 `src/mvp/README.md` 的 v2.5 使用方式(如需要) - ---- - -## 4. 关键待确认点(开始实现前必须定稿) - -1) **legacy token 是否继续兼容** -- 方案 A:保留 `MVP_INTERNAL_TOKEN`(单租户)+ 新增 user token(多租户) -- 方案 B:v2.5 直接切换到 user token(破坏兼容,但更清晰) - -2) **调度公平性** -- v2.5 先全局 FIFO(简单);后续 v3 再引入 per-user 公平调度/配额。 - -3) **head.json 的生产写入者** -- 方案 A:与 API 同进程线程(最少组件) -- 方案 B:独立进程(更独立、易运维) - diff --git a/specs/mvp/v2.5/v2.5_e2e_test_cases.md b/specs/mvp/v2.5/v2.5_e2e_test_cases.md deleted file mode 100644 index 2e8472e..0000000 --- a/specs/mvp/v2.5/v2.5_e2e_test_cases.md +++ /dev/null @@ -1,132 +0,0 @@ -# MVP v2.5 端到端测试用例(正常/异常/边界) - -本用例集目标:覆盖 v2.5 的关键能力与边界条件(User + jobs 隔离 + stateless node pool + API 队列调度)。 - -约束(v2.5 已确认): -- TaskSpec 不扩展;不支持 reward_fn;不支持用户自定义 verl 代码。 -- 输入统一 `/private/common/...`;用户隔离先只隔离 `/private/users//jobs/...` 输出。 - ---- - -## 0. 环境前置 - -远端目录示例: -- `argus@h1:/home2/argus/infra/mvp/src/mvp/` - -共享目录(宿主机): -- `/home2/argus/infra/mvp/shared/` - -容器内路径约定: -- `/private` 为共享存储挂载点 - -需要: -- GPU 0-7 可用 -- 3 容器:head(无 GPU)+ 2 worker(各 4 GPU) - ---- - -## 1. 正常用例(Happy Path) - -### HP-1:v2.5 全链路(PPO → GRPO → SFT,串行) - -步骤: -1) `cd /home2/argus/infra/mvp/src/mvp/scripts` -2) `MVP_INTERNAL_TOKEN= RESET_DB=1 ./run_all_v25_api.sh` - -期望: -- Ray dashboard 显示 3 nodes(head+2 workers),GPU 总数 8。 -- 3 个 task 最终为 `SUCCEEDED`。 -- 输出目录存在且按用户隔离: - - `/private/users//jobs//{config,logs,checkpoints,debug}` - -### HP-2:Driver 不在 head 跑 - -验证点(任选一种): -- Ray job 的 driver node IP 不等于 head 容器 IP; -- 或日志/调度信息显示 entrypoint_resources 生效(driver 在 worker)。 - ---- - -## 2. 异常用例(Error Cases) - -### E-Auth-1:缺 token - -请求: -- `GET /api/v2/queue` 不带 `Authorization` 头 - -期望: -- 返回 401(missing bearer token) - -### E-Auth-2:无效 token - -请求: -- `Authorization: Bearer ` - -期望: -- 返回 401(invalid token) - -### E-Auth-3:用户禁用后拒绝访问 - -步骤: -1) admin 创建用户 `bob` 并签发 token -2) admin 禁用 `bob` -3) 用 bob token 请求 `/api/v2/queue` - -期望: -- 返回 403(user disabled) - -### E-Isolation-1:跨用户访问 task 资源(不泄露存在性) - -步骤: -1) alice 提交 task 得到 `task_id` -2) bob 查询 `/api/v2/tasks/{task_id}` - -期望: -- 返回 404(task not found) - -### E-Input-1:输入路径不在 /private/common(v2.5 约束) - -请求: -- 提交 taskspec 但 `train_file` 或 `code_path` 不以 `/private/common/` 开头 - -期望: -- 返回 400,并给出具体字段错误(例如 `train_file must start with /private/common/`)。 - ---- - -## 3. 边界用例(Boundary) - -### B-Queue-1:资源不足时不提交 Ray(PENDING_RESOURCES) - -步骤: -1) 构造任务需求 `nnodes=3` 且 `n_gpus_per_node=4`(total 12 GPU) -2) 提交后轮询状态 - -期望: -- task 进入 `PENDING_RESOURCES`(服务侧 pending,不向 Ray submit) -- 具备 `next_run_at` - -### B-Cancel-1:任务取消(QUEUED/RUNNING) - -步骤: -1) 提交一个较长 steps 的任务(确保有机会 RUNNING) -2) 调用 `POST /api/v2/tasks/{task_id}:cancel` - -期望: -- task state 为 `CANCELED` -- attempt 中 `ray_status` 最终为 `STOPPED`(或 Ray 侧停止) - ---- - -## 4. 可执行回归脚本 - -见: -- `src/mvp/scripts/run_e2e_v25_cases.sh` - -脚本覆盖: -- HP-1 -- E-Auth-1/E-Auth-2/E-Input-1 -- E-Isolation-1 -- B-Queue-1 -- B-Cancel-1(best-effort) - diff --git a/specs/mvp/v2.5/v2.5_summary.md b/specs/mvp/v2.5/v2.5_summary.md deleted file mode 100644 index deb52fa..0000000 --- a/specs/mvp/v2.5/v2.5_summary.md +++ /dev/null @@ -1,92 +0,0 @@ -# MVP v2.5 迭代总结(已落地) - -本文档总结 v2.5 在 v2.0/v2.1/v2.2…基础上完成的能力、实现点、验收方式与已知限制,便于回顾与后续版本迭代对齐。 - -## 目标与边界 - -v2.5 的核心目标: -- 引入 **User Management(用户管理)**:基于 token 的鉴权与任务级隔离(“只隔离 jobs”)。 -- 引入 **Stateless Ray Node Pool(无状态 Ray worker 池)**:worker 不依赖平台下发 head 地址,自动发现并连接/自愈。 -- 保持 **TaskSpec(v1.1 同款 YAML 格式)不扩展**:本迭代不支持 reward function、自定义 verl 代码等。 - -明确不做(v2.5 约束): -- 不支持 TaskSpec 扩展(例如 `reward_fn_path` 等)。 -- 不支持用户自定义 verl/hf/dataset 的隔离或自定义路径:**统一使用 `/private/common/...`** 的公共资源。 -- 用户隔离仅覆盖 **任务与产物目录**(jobs),不覆盖 HF cache、datasets 等公共缓存。 - -## 关键能力(对外表现) - -### 1) 多用户鉴权与任务隔离 -- API 仍使用内部 `Authorization: Bearer ` 方式: - - 管理员 token 来自环境变量 `MVP_INTERNAL_TOKEN`(admin)。 - - 业务用户 token 由管理员通过 API 下发并持久化到 SQLite。 -- 用户隔离策略: - - 非管理员用户只能查询/取消/拉取日志 **自己的 task**;跨用户访问返回 404(不泄露存在性)。 - - 训练产物落盘隔离:Ray job 目录统一写入 `/private/users//jobs//...`。 - -### 2) task_id / submission_id 带用户名 -- 新任务 ID 规则:`mvp2----` -- Ray submission id(attempt)规则:`--aNN`,因此自然包含用户名。 -- 作用:Dashboard/日志/落盘目录可读性更强,便于按用户追踪和审计。 - -### 3) “无状态 worker 池”与 head 地址发现 -- Head 在共享存储写入 **head 地址文件**(例如 `head.json`),worker 通过 watchdog: - - 轮询发现 head 地址 - - 自动 `ray start --address ...` 加入集群 - - 掉线后自动重连(watchdog 自愈) -- 达成效果:在生产环境中,即使 worker 容器由算力平台创建(只提供 SSH 纳管),也能通过共享存储实现连接与自愈。 - -### 4) 任务调度:队列 + Ray Job 提交 + 状态回传 -- API 提交任务后进入 SQLite 队列,由后台 scheduler 逐个提交到 Ray(默认 `max_running_tasks=1`)。 -- Scheduler 持续轮询 Ray job 状态并回写任务状态(RUNNING/SUCCEEDED/FAILED/CANCELED)。 -- 资源不足的“可重试失败”处理: - - 针对 VERL 的 fail-fast(`Total available GPUs ... is less than total desired GPUs ...`)或集群资源不足, - 任务进入 `PENDING_RESOURCES` 并设置 `next_run_at`,按 `retry_interval_s` 周期重试。 - -## 关键实现点(工程化落地) - -### 存储与目录约定(容器内视角) -- 共享根路径统一为 `/private`(对齐生产挂载)。 -- v2.5 强约束:TaskSpec 的以下字段必须以 `/private/common/` 开头: - - `code_path` / `train_file` / `val_file` -- 公共目录(示例): - - `/private/common/hf`:HF 缓存 - - `/private/common/datasets`:训练数据(必要时通过 symlink 指向已有缓存目录复用下载) - - `/private/common/db/mvp.sqlite3`:队列与用户信息(SQLite) - - `/private/common/logs`:API / watchdog 日志 - - `/private/users//jobs/...`:用户作业产物(隔离) - -### Ray 拓扑与“head 不跑训练” -- Head 启动为管理节点(CPU/GPU=0),避免训练任务落到 head。 -- Worker 节点具备 GPU(示例:2 个 worker * 每个 4 GPU)。 -- driver 通过 `entrypoint_resources`(例如 `worker_node: 1`)强制落 worker。 - -### 部署脚本与可重复执行 -提供完整脚本链路,覆盖: -- 清理 legacy 环境、起停容器、启动 Ray head -- head discovery publisher、worker watchdog 启动与状态检查 -- 数据/模型/代码准备(幂等、可复用已有下载) -- 启动 API server(并支持 RESET_DB) -- API 方式连续提交 PPO/GRPO/SFT 并等待完成 - -代表性脚本: -- `src/mvp/scripts/run_all_v25_api.sh`:v2.5 happy-path 端到端(含重建集群、准备资源、起 API、提交 3 类任务) -- `src/mvp/scripts/run_e2e_v25_cases.sh`:在 happy-path 基础上增加鉴权/隔离/输入校验/资源不足/取消等用例 - -## 验收与测试(已通过) - -### 单元测试(本机 venv) -- `.venv/bin/python -m pytest` -- 覆盖率阈值:>= 90% - -### 远端端到端(h1) -- 在 `argus@h1:/home2/argus/infra/mvp/src/mvp/scripts` 执行: - - `MVP_INTERNAL_TOKEN=mvp-dev-token RESET_DB=1 ./run_e2e_v25_cases.sh` -- 结果:happy-path(PPO/GRPO/SFT)完成,且异常/边界用例验证通过(鉴权、跨用户隔离、输入校验、资源不足转 PENDING_RESOURCES、取消任务等)。 - -## 已知问题与后续建议 - -- `max_running_tasks=1` 会让队列中的任务在前序 RUNNING 时保持 QUEUED,这在“资源不足”边界测试里需要显式清空/取消前序任务,或接受该行为作为设计的一部分。 -- 当前仍是 SQLite 单点;后续若要 HA/水平扩展,可在 v2.6+ 引入更强的持久化与多副本(例如 Postgres/etcd)。 -- API server / watchdog 目前以脚本方式守护;后续可进一步统一为 systemd/supervisor(或平台侧守护)并补齐健康检查与告警。 - diff --git a/specs/mvp/v3.0/README.md b/specs/mvp/v3.0/README.md deleted file mode 100644 index b338179..0000000 --- a/specs/mvp/v3.0/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# MVP v3.0(Design)— WebUI + 用户数据上传/下载(SFTPGo)→ 首个可发布版本 - -本目录基于: -- `specs/mvp/mvp_roadmap_v2.md`(总体路线图) -- `specs/mvp/image/roadmap_v3.0.png`(v3.0 迭代图) -- 当前已落地的 v2.5(User Mgmt + Stateless Ray Node Pool) - -目标是在 v2.5 的基础上补齐 **用户数据闭环**(上传→训练可见→产物下载)以及最小可用的 **WebUI**,形成“可发布”的 v3.0 版本。 - -文档: -- `specs/mvp/v3.0/v3.0_design.md`:总体架构与关键机制(WebUI、SFTPGo、数据/权限模型、任务流)。 -- `specs/mvp/v3.0/v3.0_api.md`:v3.0 API 扩展设计(UI、数据、SFTPGo 管理、权限约束)。 -- `specs/mvp/v3.0/v3.0_acceptance.md`:部署/升级/验收流程与可验证标准(含故障注入与回归清单)。 -- `specs/mvp/v3.0/v3.0_dev_plan.md`:TDD 驱动的工程化开发计划(里程碑拆分、测试分层、E2E 验收)。 -- `specs/mvp/v3.0/v3.0_progress.md`:实施进展记录(每个里程碑完成后追加记录)。 diff --git a/specs/mvp/v3.0/v3.0_acceptance.md b/specs/mvp/v3.0/v3.0_acceptance.md deleted file mode 100644 index fe710e5..0000000 --- a/specs/mvp/v3.0/v3.0_acceptance.md +++ /dev/null @@ -1,55 +0,0 @@ -# MVP v3.0 — 部署与验收流程(草案) - -## 0) 环境前提 -- Ray 集群:延续 v2.5 的 head + stateless worker(自动 join) -- 共享存储:容器内挂载 `/private`(dev/prod 对齐) -- API server:宿主机代码挂载到 head 容器,在 head 容器内启动 -- 新增:SFTPGo 服务(建议容器化部署) - -## 1) 部署步骤(高层) - -1) 部署/升级 Ray 节点镜像(沿用 v2.5 的 `argus/argus-ray-node:v2.5` 或更高版本) -2) 启动 Ray 集群(compose 或平台创建容器) -3) 启动/配置 SFTPGo(挂载 `/private`) -4) 启动 API server(head 容器内) -5) 启动 WebUI(由 API server 托管) - -## 2) 验收用例(必须通过) - -### A. 用户与凭据 -1) admin 创建用户 `alice`,签发 API token -2) 系统联动在 SFTPGo 创建 `alice`(home=/private/users/alice) -3) `alice` 使用 token 登录 WebUI(或调用 `/api/v2/me` 成功) - -### B. 上传数据闭环(核心) -1) `alice` 通过 SFTP 上传数据集到 `/private/users/alice/datasets/...` -2) `alice` 通过 WebUI/API 提交任务,TaskSpec 引用该路径 -3) Ray worker 读取该数据,任务 RUNNING 并最终 SUCCEEDED - -### C. 下载产物闭环 -1) 训练完成后,产物落到 `/private/users/alice/jobs//...` -2) `alice` 通过 SFTP 下载 checkpoints/logs 成功 -3) (新增)`alice` 将需要长期保留的权重从 `jobs//...` 移动到 `models/`,确认移动后可长期存在 - -### C2. Jobs 回收站与自动清理(3 天移入回收站,7 天后永久删除) -1) 将 `jobs_trash_after_days`/`jobs_purge_after_days` 配置为较小值(例如分钟级,用于验证) -2) 训练完成进入 terminal 状态 -3) 等待 API server 内置 janitor 扫描周期后,确认对应 `jobs/` 被移动到 `trash/jobs/` -4) 在回收站窗口内,把某个文件从 `trash/jobs/` 移动到 `models/`,确认移动成功 -5) 等待超过 `jobs_purge_after_days` 后,确认 `trash/jobs/` 被永久删除 -6) 确认已移动到 `models/` 的文件不被删除 - -### D. 安全隔离(必须) -1) `bob` 不能通过 API 查询 `alice` 的 task(404) -2) `bob` 不能提交引用 `/private/users/alice/...` 的 TaskSpec(400/403) -3) `bob` 通过 SFTP 无法访问 `/private/users/alice/...`(chroot 生效) - -## 3) 故障注入(推荐通过) -1) kill worker watchdog 或 raylet → worker 自动恢复并重新加入集群 -2) 重启 head 容器 → head 重新写 `head.json`,worker 自动重连 -3) SFTPGo 重启 → 不影响 Ray 集群;用户可重新连接上传/下载 - -## 4) 回归清单(与 v2.5 一致) -- 任务队列、重试(INSUFFICIENT_RESOURCES → PENDING_RESOURCES → retry) -- PPO/GRPO/SFT 三种 workload 均可跑通 -- head 不跑训练(driver 强制落 worker) diff --git a/specs/mvp/v3.0/v3.0_api.md b/specs/mvp/v3.0/v3.0_api.md deleted file mode 100644 index 67f113b..0000000 --- a/specs/mvp/v3.0/v3.0_api.md +++ /dev/null @@ -1,109 +0,0 @@ -# MVP v3.0 — API 扩展设计(基于 v2.5) - -v3.0 的原则是:**尽量复用 v2.5 API**,只增量增加 “数据闭环” 与 “WebUI 支持” 所需的最小接口。 - -## 1) 认证与权限 - -沿用 v2.5: -- Header:`Authorization: Bearer ` -- admin token:来自 `MVP_INTERNAL_TOKEN` -- 普通用户 token:由 admin 颁发并持久化在 SQLite - -权限规则: -- 非 admin:只能访问自己的 task、自己的数据空间(`/private/users//...`)。 -- 跨用户访问返回 404(不泄露存在性)。 - -## 2) 用户与 SFTPGo 联动(管理员接口) - -### 2.1 创建用户(复用 v2.5) -`POST /api/v2/users` -- v3.0 行为:成功后,**可选**联动创建 SFTPGo 用户 - - v3.0 默认启用联动:创建 SFTPGo 用户 + 生成一次性密码(password 认证) - - v3.0 仅保留该方案(方案 A):不做外部认证/SSO 集成(留到更后续版本) - - `data.sftpgo.admin_api_base` 推荐形如:`http://argus-sftpgo:8080/api/v2`(包含 `/api/v2` 前缀) - -### 2.2 下发 token(复用 v2.5) -`POST /api/v2/users/{user_id}/tokens` - -### 2.3 禁用用户(复用 v2.5) -`POST /api/v2/users/{user_id}:disable` -- v3.0 行为:联动禁用 SFTPGo 用户(可选) - -### 2.4 SFTP 凭据管理(新增,管理员或用户自助) -(具体由你确认 v3.0 需要“用户自助”还是“管理员操作”) - -#### 重置 SFTP 密码(管理员) -`POST /api/v2/users/{user_id}/sftp:reset_password` -- 返回:一次性密码(只返回一次,服务端不保存明文) -> v3.0 先只做 password 方案;SSH public key 作为后续版本可选增强(不在 v3.0 范围)。 - -## 3) 用户自助信息(新增) - -### 3.1 获取当前用户信息 -`GET /api/v2/me` -- 返回示例: -```json -{ - "user_id": "alice", - "is_admin": false, - "paths": { - "home": "/private/users/alice", - "datasets": "/private/users/alice/datasets", - "models": "/private/users/alice/models", - "code": "/private/users/alice/code", - "jobs": "/private/users/alice/jobs", - "trash_jobs": "/private/users/alice/trash/jobs" - }, - "retention": { - "jobs_trash_after_days": 3, - "jobs_purge_after_days": 7 - }, - "sftp": { - "host": "h1.example.internal", - "port": 2022, - "username": "alice" - } -} -``` - -## 3.2 Jobs Retention 提示(新增) -为了支撑 WebUI 展示与用户预期管理,可在 `/api/v2/me` 或单独接口返回: -- `jobs_trash_after_days`:默认 3 -- `jobs_purge_after_days`:默认 7 -- `jobs_root`:`/private/users//jobs` -- `trash_jobs_root`:`/private/users//trash/jobs` -- `recommendations`:提示用户把需要长期保存的产物移动到 `models/` 或 `datasets/` - -## 4) 数据浏览/下载(可选,v3.0 最小化) - -说明:上传/下载主通道仍是 SFTP。 -WebUI 如果要提供“快速浏览/查看”,可实现只读接口(避免实现大文件上传/断点等复杂逻辑)。 - -### 4.1 列目录 -`GET /api/v2/files?path=/private/users/alice` -- 权限:path 必须在 `/private/common/` 或 `/private/users//` 下 -- 返回:文件列表(name/type/size/mtime) - -### 4.2 下载文件(小文件为主) -`GET /api/v2/files:download?path=/private/users/alice/jobs/.../logs/...` -- 返回:流式下载 -- 大文件仍建议走 SFTP - -## 5) TaskSpec 路径校验升级(v3.0 关键) - -v2.5:仅允许 `/private/common/...` -v3.0:允许 `/private/common/...` 与 `/private/users//...` - -应用字段(至少): -- `train_file` / `val_file` -- `code_path`:仍仅允许 `/private/common/...`(v3.0 不支持执行用户 code) -- 本地模型路径字段(如果引入):允许 `/private/users//models/...` - -## 6) WebUI 路由(新增) - -由 API server 托管: -- `GET /ui`:主页面 -- `GET /ui/login`:token 登录页 -- 静态资源:`/ui/static/...` - -WebUI 的所有操作均调用同源 API(不额外开 CORS)。 diff --git a/specs/mvp/v3.0/v3.0_design.md b/specs/mvp/v3.0/v3.0_design.md deleted file mode 100644 index 4edf84b..0000000 --- a/specs/mvp/v3.0/v3.0_design.md +++ /dev/null @@ -1,358 +0,0 @@ -# MVP v3.0 详细设计方案(基于 v2.5) - -## 0. 结论摘要(v3.0 要交付什么) - -v3.0 = v2.5 + **WebUI** + **用户数据上传/下载(SFTPGo)**,形成第一个可对外发布的版本: -- 用户可以通过 **SFTP** 上传数据/模型/代码(至少数据),落到 GPFS(容器内 `/private`)并对 Ray worker 可见。 -- 用户可以通过 API/WebUI 提交训练任务,任务读取自己上传的数据。 -- 用户可以下载训练产物(checkpoints/logs 等),最小闭环跑通。 - -## 1. 范围与原则 - -### 1.1 继承 v2.5 的前提(不回退) -- **Stateless Ray Node Pool**:head 写 `head.json`,worker watchdog 自动 join/自愈。 -- **User Management**:token 鉴权、任务可见性隔离(跨用户 404 不泄漏)。 -- **作业产物隔离**:Ray job 目录落到 `/private/users//jobs//...`。 -- **API server 短期运行方式**:代码在宿主机,挂载到 head 容器,在 head 容器内启动(保持现状)。 - -### 1.2 v3.0 新增目标 -1) **Data Management(SFTPGo)** - - 提供用户上传/下载入口(SFTP 为主)。 - - 数据落到 GPFS(dev 环境 NFS/GPFS,生产环境 GPFS),训练 job 在 worker 容器内可直接读取。 -2) **WebUI** - - 用户可视化创建任务、查看队列/状态/日志、查看“数据路径约定”和自己的 SFTP 信息。 - - 目标是 “可用而非豪华”,支持核心工作流。 -3) **权限闭环** - - 用户只能使用自己目录下的数据(`/private/users//...`)或公共目录(`/private/common/...`)。 - - 防止用户提交任务读取其他用户的文件路径。 - -### 1.3 v3.0 明确不做(留给 v3.5) -- 不做 “自定义 reward function / 自定义 verl 代码 / 多版本 verl 共存”(路线图 v3.5)。 -- 不做复杂 Serving/训推一体(路线图 v3.5)。 -- 不做 IB 网络/拓扑优化(路线图 v3.5)。 -- 不做系统级可观测性平台(路线图 v4.0)。 - -## 2. 架构概览 - -参考 `roadmap_v3.0.png`,v3.0 的控制面与数据面: - -### 2.1 控制面(Control Plane) -- **API Server(FastAPI)** - - v2.5 的任务队列/调度/重试 + 用户管理能力继续复用 - - 新增:数据管理能力(与 SFTPGo 对接) + WebUI -- **WebUI** - - 通过 API 使用 token 登录 - - 提供任务/日志/数据入口(不直接运行训练) -- **Ray Head(状态节点)** - - 仍在 head 容器内(或单独节点) - - job server/dashbaord 提供 job submit/status/logs 能力 - -### 2.2 数据面(Data Plane) -- **GPFS(容器内挂载 `/private`)** - - 存放 common 与 users 两大根目录 -- **Ray Worker Node(无状态)** - - 自动连接 head,执行训练 - - 读取 `/private/users//...` 的数据 - -### 2.3 新增组件:SFTPGo(Data Management) -- 作为独立服务运行(容器化优先),后端存储使用 **filesystem**(GPFS 挂载路径)。 -- 用户的 home directory 指向 `/private/users/`(或其子目录)。 - -## 3. 存储与目录规范(v3.0 统一约定) - -### 3.1 目录层级 -统一以容器内 `/private` 作为根路径(dev/prod 对齐): -- `/private/common/`:公共资源 - - `hf/`:HF cache - - `datasets/`:公共数据集(可选) - - `code/`:公共代码(例如公共 verl repo snapshot) - - `db/`:SQLite(队列、用户、token) - - `logs/`:API/supervisor/watchdog 日志 -- `/private/users//`:用户空间(v3.0 重点) - - `datasets/`:用户上传的数据集(推荐) - - `models/`:用户保存/上传的本地模型(允许;也用于“把 job 产物移动到长期保存目录”) - - `code/`:用户上传的代码(v3.0 **不支持执行**;仅存放/下载) - - `jobs/`:训练任务产物(已在 v2.5 落地) - - `tmp/`:临时文件(可选) - -### 3.2 Jobs Retention(两段式:3 天移入回收站,7 天后永久删除) -v3.0 引入 **jobs 目录两段式保留策略**: -- 第 1 阶段(soft-delete):job 结束后 **3 天**,将该 job 目录从 `jobs/` **移动到用户回收目录**; -- 第 2 阶段(hard-delete):进入回收目录后再过 **7 天**,从回收目录 **永久删除**。 - -目录约定(建议): -- jobs 根目录:`/private/users//jobs//...` -- 回收目录:`/private/users//trash/jobs//...` - -计时规则: -- 以 job 进入 terminal 状态(SUCCEEDED/FAILED/CANCELED)的结束时间为起点; -- “3 天”用于从 `jobs/` 移入 `trash/jobs/`; -- “7 天”用于从 `trash/jobs/` 永久删除(即总共最多 10 天窗口)。 - -用户保留关键产物的方式(无需 keep 标记): -- 在 “3 天窗口”内把需要长期保存的文件从 `jobs//...` **移动/复制**到 `models/`(例如权重)或 `datasets/`(例如评估输出数据); -- 即便已被移动到回收目录,用户仍可在 “7 天窗口”内从 `trash/jobs//...` 把需要的文件移到 `models/` / `datasets/`; -- janitor 只管理 `jobs/` 与 `trash/jobs/`,不会触碰 `models/` 与 `datasets/`。 - -这里的“清理程序”我们称为 **janitor**: -- 定义:一个后台清理执行器,按固定周期扫描“已结束且已过期”的 job 目录并删除 -- v3.0 目标:实现“3 天移入回收站 + 7 天后删除”这一条产品规则(不提供 keep/延长保留标记) - -实现建议(按你的偏好): -- **janitor 作为 API server 内置后台线程**运行: - - 优点:天然可访问 SQLite(任务状态、结束时间、user_id、ray_submission_id),并能把清理结果写回 events 表用于审计 - - 部署更简单:不额外引入 cronjob/独立服务 -- 删除/移动动作建议 **直接在 GPFS/NFS 文件系统上操作**(API server 运行在 head 容器,已挂载 `/private`): - - 第 1 阶段:`os.rename`(同文件系统原子移动)把 `jobs/` 移到 `trash/jobs/`; - - 若跨文件系统(理论上不应发生),则降级为 copy+delete; - - 移动前做严格路径前缀校验(必须在 `.../users//jobs/` 下)。 - - 第 2 阶段:对 `trash/jobs/` 执行递归删除(例如 `shutil.rmtree`),同样做路径前缀校验(必须在 `.../users//trash/jobs/` 下)。 - - 为什么不依赖 SFTPGo API:SFTPGo 只是用户访问协议层(SFTP/Web),目录物理就在同一份文件系统;文件系统直连更简单、也不依赖 SFTPGo 在线。 -- 如果你强烈希望“通过 SFTPGo API 删除”: - - 可以作为可选实现/补充(例如用于统一审计或未来接入配额/策略),但不建议作为唯一手段(SFTPGo 停机不应阻塞清理)。 - -### 3.3 用户在 SFTPGo 内移动/整理文件(确认点) -支持用户在 SFTPGo 中进行“移动/重命名/整理”(例如把权重从 `jobs/` 移动到 `models/`): -- 前提:SFTPGo 用户权限允许对其 home 目录进行 `rename/mkdir/remove` 等操作(v3.0 默认可写)。 -- 行为:用户可以把 `jobs/` 下某些文件移动到 `models/` 或 `datasets/`,用于长期保存权重/评估产物等。 -- 与 retention 的关系:只要文件被移动出 `jobs/`,就不会被 jobs 清理逻辑删除。 - -### 3.4 路径权限规则(API 侧校验) -v2.5 约束是 “只允许 `/private/common/...`”。 -v3.0 需要升级为: -- 允许: - - `/private/common/...` - - `/private/users//...` -- 禁止: - - 任何其他绝对路径(例如 `/private/users/other/...`、`/etc/...`) - -并把该规则应用到 TaskSpec 的相关字段(至少): -- `train_file` / `val_file` -- `code_path`:仍仅允许 `/private/common/...`(v3.0 不支持执行用户 code) -- 本地模型路径字段:允许 `/private/users//models/...`(确认:v3.0 允许) - -## 4. SFTPGo 方案设计(Data Management) - -### 4.1 运行形态 -推荐用容器运行 SFTPGo(与 Ray/API 解耦),挂载同一份 `/private`: -- `sftpgo` 容器挂载 `../../shared:/private` -- 对外暴露: - - SFTP 端口(建议 2022) - - WebAdmin/API 端口(建议 8081,仅内网或管理员访问) - -#### 4.1.1 镜像来源(现成 Docker 镜像) -SFTPGo 有现成可用的 Docker 镜像(无需自建): -- v3.0 推荐优先使用官方/上游发布的 `sftpgo` 镜像作为运行基座 -- 我们在 v3.0 里不需要定制 SFTPGo 代码,只需要: - - 正确挂载 GPFS/NFS(容器内 `/private`) - - 配置管理员账号(用于 API server 联动创建/禁用用户、重置密码) - - 配置每用户 home/chroot - -> 注意:具体镜像名/tag 在不同环境可能有差异(官方/镜像仓库策略会变动)。落地时建议在 `argus@h1` 上 `docker search sftpgo` 或由你们内部镜像仓库提供固定版本;v3.0 设计只要求“使用现成镜像”,不强依赖某个 tag。 - -#### 4.1.2 docker-compose 服务草案(示意) -下面给出一个**示意**(最终以实际镜像名/tag 与你们端口规划为准): - -```yaml -services: - sftpgo: - image: sftpgo/sftpgo:latest # 示例:使用现成镜像 - container_name: argus-sftpgo - ports: - - "2022:2022" # SFTP - - "8081:8080" # WebAdmin/API(建议仅内网/管理员) - volumes: - - ../../shared:/private - - ../../shared/common/sftpgo:/var/lib/sftpgo # 持久化 SFTPGo 元数据(可选/建议) - environment: - # 管理员账号/密码(示意,具体变量名以镜像文档为准) - SFTPGO_ADMIN_USERNAME: "admin" - SFTPGO_ADMIN_PASSWORD: "${SFTPGO_ADMIN_PASSWORD}" -``` - -与 v3.0 的配合点: -- API server 使用 `data.sftpgo.admin_api_base` + admin 凭据联动创建用户 -- 用户 home/chroot 统一指向 `/private/users/` - -### 4.2 用户隔离 -每个用户在 SFTPGo 中的 home dir 绑定到: -- `/private/users/`(chroot),用户只能读写自己的目录。 - -### 4.3 用户创建与凭据管理(两种实现,建议先做 A) - -**方案 A(v3.0 推荐):API Server 负责“联动创建 SFTPGo 用户”** -- 在 v2.5 的 `POST /api/v2/users` 成功后: - - API server 调用 SFTPGo 管理 API 创建同名用户 - - 设置 home dir = `/private/users/` - - 设置权限(默认可写;是否只读可配置) -- 认证方式: - - v3.0 最小可用:用户名+密码(确认:v3.0 先 password;API 生成一次性密码,用户首次登录后要求改密) - - 或:SSH public key(WebUI 允许上传 public key,API 写入 SFTPGo) - -**方案 B(更强但复杂):SFTPGo 外部认证** -- SFTPGo 把认证委托给 API server(token/SSO),SFTP 也走内部 token。 -- 复杂度高,建议 v3.0 不做,放到 v3.5 或更后。 - -### 4.4 用户上传/下载体验 -用户通过 SFTP 上传: -- `datasets/...`(训练数据) -- `models/...`(本地模型,可选) -下载: -- `jobs//...`(checkpoints/logs) - -WebUI/文档提供 “路径如何写进 TaskSpec” 的指引。 - -## 5. WebUI 方案设计(最小可用) - -### 5.1 目标页面 -v3.0 WebUI 采用“**多子页面 + 侧边导航栏**”而不是把所有功能挤到单页: -- 原因:信息密度更可控,后续可扩展(v3.5+)且不会把一个页面做成“巨型表单/巨型列表”。 -- 实现仍保持轻量:服务端渲染(或静态 HTML + 少量 JS),不引入复杂前端工程。 - -信息架构(IA)建议如下: -1) **登录页**(`/ui/login`) - - 用户粘贴 token(管理员发放),浏览器保存(localStorage/sessionStorage) - - 提供“退出登录/清空 token” -2) **任务列表页**(`/ui/tasks`) - - 默认列表:最近 N 条任务(按 created_at 倒序) - - 支持过滤:workload、state(QUEUED/RUNNING/SUCCEEDED/FAILED/CANCELED)、时间范围 - - 支持快捷操作:进入详情、取消任务 -3) **新建任务页**(`/ui/tasks/new`) - - 两种模式(二选一,均可实现): - - **YAML 直接提交**:上传/粘贴 TaskSpec YAML(最省开发) - - **表单生成 YAML**:选择 workload,填写核心字段(train/val/model/nnodes/gpus),生成 YAML 预览后提交 - - 提交后跳转到任务详情页 -4) **任务详情页**(`/ui/tasks/{task_id}`) - - 顶部:task_id、workload、state、created_at、updated_at、error_summary - - Attempt 卡片:latest attempt_no、ray_submission_id、ray_status、start/end - - 操作区:取消任务(若非 terminal)、刷新状态、复制路径/ID - - 链接到日志页与产物提示(SFTP 路径) -5) **任务日志页**(`/ui/tasks/{task_id}/logs`) - - 默认 tail=2000,可选 200/1000/5000 - - 提供“自动刷新(每 3~5 秒)”开关(简单轮询即可) -6) **数据页**(`/ui/data`) - - 显示 SFTP 连接信息(host/port/username) - - 显示用户目录约定: - - home:`/private/users/` - - datasets:`/private/users//datasets` - - models:`/private/users//models` - - jobs:`/private/users//jobs` - - trash/jobs:`/private/users//trash/jobs` - - 明确 retention:jobs 结束后 3 天移入回收站,回收站 7 天后删除;重要文件请移到 `models/` 或 `datasets/` -7) **(仅管理员可见)用户管理页**(`/ui/admin/users`,可选但很有价值) - - 创建用户、禁用用户、签发 token、重置 SFTP 密码(方案 A) - -### 5.2 页面组织与导航(建议) -侧边栏导航(普通用户): -- Tasks(列表) -- New Task(新建) -- Data(SFTP/目录说明) - -管理员侧边栏额外增加: -- Admin / Users - -### 5.3 大致示意图(wireframe) - -下面是一个粗略示意(非最终 UI,仅表达信息结构与布局): - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ Argus MVP v3.0 [user: alice] │ -├───────────────┬──────────────────────────────────────────────────────┤ -│ Side Nav │ /ui/tasks │ -│ │ │ -│ • Tasks │ [Filter] workload=all state=all [Search task_id] │ -│ • New Task │ │ -│ • Data │ Task List │ -│ • Admin(*) │ ┌────────────────────────────────────────────────┐ │ -│ │ │ task_id workload state ... │ │ -│ │ │ mvp2-alice-ppo-... ppo RUNNING ... │ │ -│ │ │ mvp2-alice-sft-... sft SUCCEEDED... │ │ -│ │ └────────────────────────────────────────────────┘ │ -│ │ [View] [Cancel] │ -└───────────────┴──────────────────────────────────────────────────────┘ -``` - -任务详情页(示意): -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ /ui/tasks/{task_id} │ -├──────────────────────────────────────────────────────────────────────┤ -│ task_id: mvp2-alice-ppo-... state: RUNNING workload: ppo │ -│ created_at: ... updated_at: ... │ -│ error_summary: (empty) │ -│ │ -│ latest_attempt: a01 ray_submission_id: ...--a01 ray_status: RUNNING │ -│ [Open Logs] [Cancel Task] [Refresh] │ -│ │ -│ Artifacts (SFTP paths): │ -│ jobs/: /private/users/alice/jobs// │ -│ trash/: /private/users/alice/trash/jobs// │ -│ tip: move important files to /private/users/alice/models/ │ -└──────────────────────────────────────────────────────────────────────┘ -``` - -### 5.2 技术取舍(建议:不引入 Node 构建) -为了降低部署复杂度,建议 v3.0 WebUI 以 “服务端渲染 + 少量 JS/HTMX” 或 “纯静态 HTML+fetch” 实现: -- 由 API server 提供静态资源(FastAPI StaticFiles) -- 页面调用同源 API,避免跨域与复杂前端构建链 - -## 6. API 扩展设计(概览) - -v3.0 可以保持 `/api/v2/...` 不变,增量加: -- SFTPGo 集成管理端点(管理员): - - 创建/禁用用户时联动 SFTPGo - - 重置 SFTP 密码 / 更新 SSH key -- 用户数据端点(可选,最小化): - - `/api/v2/me`:返回 user_id、SFTP 信息(host/port/home) - - `/api/v2/files`:仅用于浏览/下载(上传仍走 SFTP) - -详细见 `specs/mvp/v3.0/v3.0_api.md`。 - -## 7. 配置与部署(v3.0 新增项) - -在 `configs/dev.yaml` 基础上扩展一组 `data` 配置(示意): -```yaml -data: - shared_root: "/private" # 通常与 ray.shared_root 一致 - user_root: "/private/users" # 用户空间根目录 - allow_common_prefix: "/private/common/" - allow_user_prefix_template: "/private/users/{user_id}/" - - sftpgo: - enabled: true - host: "127.0.0.1" - sftp_port: 2022 - admin_api_base: "http://127.0.0.1:8081/api/v2" - admin_user: "admin" - admin_password_env: "SFTPGO_ADMIN_PASSWORD" # 仅 head 容器内可读 - - retention: - jobs_trash_after_days: 3 - jobs_purge_after_days: 7 - trash_root_template: "/private/users/{user_id}/trash/jobs" - janitor_interval_s: 3600 # 每小时扫一次(可配置) -``` - -## 8. 风险点与对策 - -1) **路径逃逸/越权读取** - - 必须在 API 提交任务时校验路径前缀 - - SFTPGo 必须 chroot 到用户 home -2) **大文件上传稳定性** - - 优先用 SFTP(断点续传/可靠性更好) -3) **用户 token 与 SFTP 凭据的生命周期** - - token 走 v2.5 SQLite - - SFTP 凭据建议独立(密码/SSH key),并提供 reset 流程 -4) **GPFS/NFS 权限** - - 确保 `/private/users/` 目录权限可被 SFTPGo 写入且 worker 可读 - -## 9. 已确认结论(来自你的反馈) -1) 允许用户上传并在训练时使用自定义数据集:允许(`/private/users//datasets/...`)。 -2) 允许用户上传并在训练时使用本地模型路径:允许(`/private/users//models/...`)。 -3) v3.0 不允许执行用户自定义代码(不注入 `PYTHONPATH` 作为可执行 code path)。 -4) SFTPGo 认证方式:v3.0 先 password。 -5) WebUI:按“简单最小必要功能”做(token 粘贴登录优先)。 - -## 10. 待确认问题(需要你给结论) -(已确认)jobs 清理执行主体:v3.0 采用 **API server 内置 janitor 后台线程**。 diff --git a/specs/mvp/v3.0/v3.0_dev_plan.md b/specs/mvp/v3.0/v3.0_dev_plan.md deleted file mode 100644 index 9a14b4b..0000000 --- a/specs/mvp/v3.0/v3.0_dev_plan.md +++ /dev/null @@ -1,232 +0,0 @@ -# MVP v3.0 开发计划(TDD 驱动) - -本文是 v3.0 的**工程化开发计划**,强调“先写测试,再写实现”(TDD),并将每个里程碑拆成**可独立验收**的小闭环。 - -输入依据: -- 路线图:`specs/mvp/mvp_roadmap_v2.md` -- v3.0 设计:`specs/mvp/v3.0/v3.0_design.md` -- v3.0 API:`specs/mvp/v3.0/v3.0_api.md` -- v3.0 验收:`specs/mvp/v3.0/v3.0_acceptance.md` -- 现状基线:v2.5(Task queue + User mgmt + Stateless ray pool + 单镜像节点守护) - -v3.0 已确认约束: -- 允许用户数据集路径:`/private/users//datasets/...` -- 允许用户本地模型路径:`/private/users//models/...` -- **不允许执行用户自定义代码**(不注入 user code 到 PYTHONPATH;`code_path` 仍只允许 `/private/common/...`) -- SFTPGo 先用 **password** 方案(方案 A:API 联动创建/管理 SFTPGo 用户) -- jobs retention:**3 天移入回收站(trash/jobs),再 7 天永久删除**;不提供 keep/延长保留标记 -- janitor:**API server 内置后台线程**;删除/移动采用**文件系统直接操作**(不依赖 SFTPGo API) - ---- - -## 0. TDD 规范(所有功能都遵循) - -### 0.1 测试分层 - -1) **单元测试(fast)** -- 纯 Python 逻辑:路径策略、SFTPGo client、retention 计算、文件移动/删除策略(用临时目录)。 -- 不依赖真实 Ray、不依赖 docker、不依赖网络。 - -2) **组件测试(中等)** -- FastAPI 路由(含 WebUI 路由):`fastapi.testclient.TestClient` -- mock/stub SFTPGo client 与 ray client - -3) **端到端(慢)** -- 在 `argus@h1` 通过 docker compose + scripts: - - Ray 集群自动起来(head+2 worker) - - SFTPGo 服务可用 - - 上传数据 → 提交训练 → 下载产物 → jobs 回收站/清理 - -### 0.2 代码与测试约定 -- 测试目录:`src/mvp/py/tests/` -- 新功能必须先补齐测试用例,并让其在未实现时失败(红) -- 最小实现让测试变绿(绿) -- 再做重构(重构) -- 覆盖率:继续沿用当前阈值(>= 90%) - ---- - -## 1. 里程碑拆分(v3.0 = 5 个可验证闭环) - -### M1:TaskSpec 路径策略升级(允许 user datasets/models;code_path 仍仅 common) - -**目标** -- API submit 时的路径校验从 v2.5 的 “仅 `/private/common/`” 升级为: - - `train_file` / `val_file`:允许 `/private/common/...` 与 `/private/users//...` - - 本地模型路径:允许 `/private/users//models/...`(不改变 YAML 结构,见实现建议) - - `code_path`:仍仅允许 `/private/common/...` -- 阻止越权路径(`/private/users/other/...`)与非 `/private/...` 路径。 - -**实现建议(不扩展 TaskSpec)** -- `model_id` 字段保持不变: - - 若 `model_id` 以 `/private/` 开头 → 视作本地模型路径 - - 否则视作 HuggingFace repo id(如 `Qwen/...`) - -**TDD 用例(先写测试)** -- 单测: - - `test_paths_allow_common_and_own_user_prefix()` - - `test_paths_reject_other_user_prefix()` - - `test_model_id_local_path_allowed_only_under_users_models()` - - `test_code_path_still_common_only()` -- API 测试: - - `test_submit_accepts_user_datasets_paths()` - - `test_submit_rejects_cross_user_paths_404_or_400()`(按约定返回 400/403) - -**验收点** -- `v3.0_acceptance.md` 的 D 类安全隔离用例可由 API 测试覆盖。 - ---- - -### M2:SFTPGo 集成(方案 A:用户联动创建 + password) - -**目标** -- 引入 `data management (SFTPGo)`: - - admin 创建用户时联动创建 SFTPGo 用户(home=/private/users/,chroot) - - password 模式:生成一次性密码(reset/create)并返回给 admin(明文只返回一次) -- 提供用户自助信息: - - `GET /api/v2/me` 返回 SFTP 连接信息、目录约定、retention 提示。 - -**实现建议** -- 新增 `SFTPGoAdminClient`(同步调用): - - 通过 `urllib` 或 `httpx`(建议 `urllib`,减少依赖;禁止 hard-code requests 使用) - - 支持:create user / disable user / reset password(最小集合) -- API server 启动时校验配置(enabled 时必须具备 admin 密码 env)。 -- 同步创建用户目录结构(文件系统): - - `/private/users//{datasets,models,code,jobs,trash/jobs}`(最小必需) - -**TDD 用例(先写测试)** -- 单测: - - `test_sftpgo_client_builds_correct_requests()`(不发真实网络;mock urlopen) - - `test_user_dirs_created_on_user_create()`(tmp dir 断言目录存在) -- API 测试: - - `test_create_user_calls_sftpgo_client()`(stub client,断言调用参数) - - `test_me_returns_sftp_info_and_paths()`(含 trash/jobs 与 TTL 字段) - -**验收点** -- `v3.0_acceptance.md` 的 A 类(用户/凭据)与 B 类(上传闭环前置)覆盖。 - ---- - -### M3:WebUI(最小可用,多页面 + 侧边栏) - -**目标** -- WebUI 由 API server 托管(同源,无额外 CORS): - - `/ui/login`:token 粘贴登录(localStorage) - - `/ui/tasks`:任务列表 + 过滤(最小) - - `/ui/tasks/new`:YAML 提交(优先)+(可选)表单生成 YAML - - `/ui/tasks/{task_id}`:详情页 - - `/ui/tasks/{task_id}/logs`:日志 tail + 可选自动刷新 - - `/ui/data`:SFTP 信息 + 目录/retention 提示 - - (可选)`/ui/admin/users`:管理员用户管理(若时间允许,强烈建议) - -**实现建议** -- 先不引入 Node 构建: - - HTML 模板可用最简单的字符串拼接或 Jinja2(若引入 jinja2,则补齐依赖与测试) - - 页面通过 fetch 调用 `/api/v2/...`,并复用 token header - -**TDD 用例(先写测试)** -- 组件测试(TestClient): - - `test_ui_routes_render_200()` - - `test_ui_contains_sidebar_links()`(简单断言文本包含导航链接) - - `test_ui_tasks_detail_shows_ids()`(包含 task_id、state、ray_submission_id) - -**验收点** -- WebUI 能完成:登录→创建任务→查看任务→查看日志→看到 data 页提示。 - ---- - -### M4:Jobs Retention janitor(3 天移入 trash,7 天后 purge) - -**目标** -- API server 内置 janitor 后台线程: - - 周期性扫描 DB 中 terminal tasks - - 到期后执行: - - move:`/private/users//jobs/` → `/private/users//trash/jobs/` - - purge:递归删除 `/private/users//trash/jobs/` - - 全程严格 path 校验,禁止越界删除 - - 清理操作记录到 DB events(审计) - -**实现建议(数据与状态)** -- 需要稳定的时间锚点与幂等: - - 使用 attempts.end_time 作为 job 结束时间(latest attempt) - - 在 tasks 表新增字段(或新表)记录: - - `trashed_at`(首次成功 move 时间) - - `purged_at`(成功删除时间) - - `trash_path`(可选) - - 幂等:重复运行不会报错(目录不存在视为已处理) - -**TDD 用例(先写测试)** -- 单测(用 tmpdir 构造 jobs/trash 目录): - - `test_janitor_moves_job_to_trash_after_threshold()` - - `test_janitor_purges_trash_after_threshold()` - - `test_janitor_never_touches_models_or_datasets()` - - `test_janitor_path_escape_rejected()`(恶意 path 不可删) -- API/组件测试: - - `test_me_includes_retention_fields()`(jobs_trash_after_days/jobs_purge_after_days) - -**验收点** -- `v3.0_acceptance.md` 的 C2 用例可按“把阈值调小到分钟级”完成验证。 - ---- - -### M5:端到端(h1)— SFTP 上传→训练→产物下载→回收站/清理 - -**目标** -- 在 `argus@h1` 落一个一键脚本(或手册)跑通: - 1) `docker compose up -d` 拉起 Ray(head+2 worker)+ SFTPGo - 2) admin 创建用户 alice(联动创建 SFTPGo 用户 + password) - 3) alice 通过 SFTP 上传: - - 数据集到 `/private/users/alice/datasets/...` - - (可选)本地模型到 `/private/users/alice/models/...` - 4) alice 通过 API/WebUI 提交任务引用上述路径 - 5) 任务成功后: - - 从 `jobs/` 下载 logs/checkpoints - - 把权重移动到 `models/`,验证不会被清理 - 6) 把 retention 配置调小,验证 jobs→trash→purge - -**交付建议** -- 新增脚本(命名示例): - - `scripts/run_all_v30_api.sh` - - `scripts/run_e2e_v30_cases.sh` -- 新增 `docker-compose.yaml` 中的 `sftpgo` service(或 `docker-compose.v30.yaml` 叠加文件) - -**验收点** -- `v3.0_acceptance.md` 全部 MUST 用例通过。 - ---- - -## 2. 风险与测试关注点 - -1) **权限与路径逃逸** -- path policy 必须覆盖:train/val/model_id(local)/output dirs(jobs/trash) -- 所有删除/移动必须做 prefix 校验 - -2) **并发与竞态** -- janitor 只处理 terminal tasks,避免清理正在写入的目录 -- move 使用同文件系统 `os.replace`(原子) - -3) **SFTPGo 可用性** -- SFTPGo 不在线不应影响训练与 API 核心功能(除了用户创建联动) -- janitor 不依赖 SFTPGo(文件系统直连) - ---- - -## 3. 交付清单(代码/配置/脚本/文档) - -### 3.1 代码 -- Path policy(v3.0) -- SFTPGoAdminClient + user create/disable/reset password 联动 -- `/api/v2/me` 扩展(SFTP/目录/retention) -- WebUI 路由与静态资源 -- janitor(trash+purge)后台线程 + DB 记录 - -### 3.2 配置 -- `configs/dev.yaml` 增加 `data.sftpgo`、`data.retention` 段(详见设计文档) - -### 3.3 scripts / compose -- compose 增加 `sftpgo`(或新增 overlay compose 文件) -- v3.0 e2e 脚本(上传/下载/清理验证) - -### 3.4 文档 -- 更新 `specs/mvp/v3.0/*` 与 `src/mvp/README.md`(运行方式、路径约定、SFTP 操作、retention 解释) - diff --git a/specs/mvp/v3.0/v3.0_progress.md b/specs/mvp/v3.0/v3.0_progress.md deleted file mode 100644 index 49ebfe6..0000000 --- a/specs/mvp/v3.0/v3.0_progress.md +++ /dev/null @@ -1,154 +0,0 @@ -# MVP v3.0 进展记录(milestone log) - -本文档用于记录 v3.0 按 `specs/mvp/v3.0/v3.0_dev_plan.md` 实施过程中的里程碑完成情况。 -约定:每完成一个里程碑,追加一条记录,包含**日期**、**完成内容**、**涉及文件**、**验证方式/结果**、**待办/风险**。 - ---- - -## M1:Path policy + tests(已完成) - -- 日期:2025-12-30 -- 范围:按 v3.0 路径策略升级 API submit 的路径校验(不扩展 TaskSpec YAML 结构)。 -- 完成内容: - - `code_path`:仍只允许 `/private/common/...`(v3.0 不执行 user code)。 - - `train_file`/`val_file`:允许 `/private/common/datasets/...` 或 `/private/users//datasets/...`。 - - `model_id`:若以 `/private/` 开头则视为本地路径,仅允许: - - `/private/common/models/...` 或 - - `/private/users//models/...` - 否则仍按 HuggingFace repo id(如 `Qwen/...`)处理。 - - 拒绝跨用户路径(例如 `bob` 提交 `/private/users/alice/datasets/...`)。 - - 拒绝本地模型路径不在 `models/`(例如指向 `jobs/`)。 -- 涉及文件: - - `src/mvp/py/argus/service/app.py` - - `src/mvp/py/tests/test_users.py` -- 验证方式与结果: - - 本地单测:`.venv/bin/python -m pytest -q` - - 结果:全部通过(`54 passed`),覆盖率阈值保持 `>= 90%`。 -- 待办/风险: - - `model_id=/private/...` 的“本地模型路径语义”需要在用户文档/WebUI 中明确提示(避免误用)。 - - 后续 M2/M3 需要把该路径策略同步到 UI 表单/提示文本(避免用户填错路径)。 - ---- - -## M2:SFTPGo 集成(方案 A:用户联动创建 + password)(已完成) - -- 日期:2025-12-30 -- 范围:SFTPGo(Data Management)最小集成 + 用户自助信息 `/api/v2/me` + 用户目录结构落盘。 -- 完成内容: - - 新增 `data` 配置段: - - `data.user_root`:用户数据根目录(默认 `/private/users`) - - `data.sftpgo`:SFTPGo 可选联动(enabled/host/sftp_port/admin_api_base/admin_user/admin_password_env) - - `data.retention`:jobs 过期策略配置(3 天移入 trash,7 天 purge;janitor 在 M4 实现) - - 新增 `SFTPGoAdminClient`(`urllib` 实现,不使用 `requests`): - - `create_user` / `disable_user` / `reset_password`(最小集合) - - API server 增强: - - `POST /api/v2/users`:创建 DB user + 同步创建目录结构(`datasets/models/code/jobs/trash/jobs`) - - 当 `data.sftpgo.enabled=true` 时,创建用户会联动调用 SFTPGo admin API,并返回一次性密码(明文仅返回一次,服务端不保存) - - `POST /api/v2/users/{user_id}:disable`:禁用用户(SFTPGo 禁用 best-effort) - - `POST /api/v2/users/{user_id}/sftp:reset_password`:管理员重置一次性密码(SFTPGo enabled 才允许) - - `GET /api/v2/me`:返回当前用户的目录约定、retention 提示,以及(可选)SFTP 连接信息 - - 同步更新 `src/mvp/configs/dev.yaml`:补齐 v3.0 相关 `data.*` 配置(默认关闭 sftpgo)。 -- 涉及文件: - - `src/mvp/py/argus/service/config.py` - - `src/mvp/py/argus/service/sftpgo.py` - - `src/mvp/py/argus/service/app.py` - - `src/mvp/py/tests/test_sftpgo.py` - - `src/mvp/py/tests/test_users.py` - - `src/mvp/py/tests/test_app.py` - - `src/mvp/py/tests/test_service_config.py` - - `src/mvp/configs/dev.yaml` - - `specs/mvp/v3.0/v3.0_api.md` -- 验证方式与结果: - - 本地单测:`.venv/bin/python -m pytest -q` - - 结果:全部通过(`62 passed`),覆盖率 `90.11%`(阈值 `>= 90%`)。 -- 待办/风险: - - M2 仅做了“API 侧联动 + 单测”,未在真实 SFTPGo 容器上端到端验证(按计划在 M5 完成)。 - - 目录创建依赖文件系统权限:生产部署时需确保 API/head 容器对 `/private/users` 可写。 - ---- - -## M3:WebUI(最小可用,多页面 + 侧边栏)(已完成) - -- 日期:2025-12-30 -- 范围:API server 托管最小 WebUI(同源,不引入 Node 构建),用于登录/提交/查看任务与日志、查看 data 信息。 -- 完成内容: - - 新增 UI 路由(HTML+少量 JS): - - `/ui`(重定向到 tasks) - - `/ui/login`:token 粘贴并写入浏览器 localStorage(key=`mvp_token`) - - `/ui/tasks`:任务队列列表(调用 `/api/v2/queue`) - - `/ui/tasks/new`:提交 TaskSpec YAML(POST `/api/v2/tasks`) - - `/ui/tasks/{task_id}`:任务详情(GET `/api/v2/tasks/{task_id}`,支持 cancel) - - `/ui/tasks/{task_id}/logs`:日志查看(GET `/api/v2/tasks/{task_id}/logs`,可选自动刷新) - - `/ui/data`:展示 `/api/v2/me` 返回的路径/SFTP/retention 信息 - - 统一侧边栏导航:Tasks / New Task / Data / Login。 - - UI 不做服务端 session:所有 API 调用均由浏览器带 `Authorization: Bearer `(localStorage 注入)。 -- 涉及文件: - - `src/mvp/py/argus/service/ui.py` - - `src/mvp/py/argus/service/app.py` - - `src/mvp/py/tests/test_ui.py` -- 验证方式与结果: - - 本地单测:`.venv/bin/python -m pytest -q` - - 结果:全部通过(`65 passed`),覆盖率 `90.53%`(阈值 `>= 90%`)。 -- 待办/风险: - - WebUI 当前为“骨架+API 驱动”,不做复杂交互与大文件下载;上传/下载仍以 SFTP 为主(按设计)。 - - Starlette TestClient 的 `allow_redirects` 有弃用告警(不影响功能,可在后续清理)。 - ---- - -## M4:Jobs Retention janitor(3 天移入 trash,7 天后 purge)(已完成) - -- 日期:2025-12-30 -- 范围:API server 内置后台线程,对“已结束 attempt”的 job 目录执行保留策略(文件系统直连,不依赖 SFTPGo)。 -- 完成内容: - - 新增 `JobsJanitor`: - - 以 `attempts.end_time` 为基准计算 TTL(从 job 结束开始算) - - `>= 3 天 && < 7 天`:把目录从 `.../jobs/` 移动到 `.../trash/jobs/` - - `>= 7 天`:确保目录进入 trash 后删除(`shutil.rmtree`) - - 对缺失目录、异常移动/删除为 best-effort(不影响服务主流程) - - DB 增强:新增查询 `list_ended_attempts_before()`,用于 janitor 扫描候选 attempt。 - - API server 启动时启动 janitor 线程(可通过 `data.retention.janitor_interval_s` 控制;<=0 视为关闭)。 -- 涉及文件: - - `src/mvp/py/argus/service/janitor.py` - - `src/mvp/py/argus/service/db.py` - - `src/mvp/py/argus/service/app.py` - - `src/mvp/py/tests/test_janitor.py` -- 验证方式与结果: - - 本地单测:`.venv/bin/python -m pytest -q` - - 结果:全部通过(`75 passed`),覆盖率 `90.72%`(阈值 `>= 90%`)。 -- 待办/风险: - - M4 只做“逻辑 + 单测”,实际 `/private/users/...` 的权限与在 `argus@h1` 的行为验证放到 M5(端到端)。 - ---- - -## M5:端到端(h1)— SFTPGo compose + v3.0 E2E 脚本(已完成:交付脚本/配置) - -- 日期:2025-12-30 -- 范围:补齐 h1 端到端所需的 compose/service、配置与一键脚本(实际运行/验收由你在 `argus@h1` 执行)。 -- 完成内容: - - SFTPGo 集成到 `docker compose`: - - 新增 `argus-sftpgo` service(SFTP 2022;Admin API/UI 8080→host 8081,避免与 MVP API 8080 冲突) - - 同挂载 `../../shared:/private`,并持久化元数据到 `../../shared/common/sftpgo` - - SFTPGoAdminClient 实装(对齐 upstream OpenAPI): - - `GET /api/v2/token`(BasicAuth)获取 admin token - - `POST /api/v2/users` 创建用户(含 `permissions: {"/":["*"]}`) - - `PUT /api/v2/users/{username}` 禁用/重置密码 - - 新增 v3.0 dev 配置:`configs/dev_v30.yaml`(启用 `data.sftpgo` 并配置 `admin_api_base=http://argus-sftpgo:8080/api/v2`) - - 新增 v3.0 一键脚本: - - `scripts/run_all_v30_api.sh`:起 Ray+SFTPGo、启动 API、创建用户并提交 PPO/GRPO/SFT(引用 user dataset 路径) - - `scripts/run_e2e_v30_cases.sh`:最小 E2E runner(HP-1) - - API 启动脚本增强:`scripts/60_start_api.sh` 支持透传 `SFTPGO_ADMIN_PASSWORD` 到 head 容器内的 API 进程。 -- 涉及文件: - - `src/mvp/docker-compose.yaml` - - `src/mvp/configs/dev_v30.yaml` - - `src/mvp/scripts/run_all_v30_api.sh` - - `src/mvp/scripts/run_e2e_v30_cases.sh` - - `src/mvp/scripts/60_start_api.sh` - - `src/mvp/py/argus/service/sftpgo.py` - - `src/mvp/py/tests/test_sftpgo.py` - - `src/mvp/README.md` - - `specs/mvp/v3.0/v3.0_api.md` -- 验证方式与结果: - - 本地单测:`.venv/bin/python -m pytest -q` - - 结果:全部通过(`75 passed`),覆盖率 `90.35%`(阈值 `>= 90%`)。 -- 待办/风险: - - 需要你在 `argus@h1` 实跑 `scripts/run_all_v30_api.sh` 完成真正的 SFTP 上传/下载与 retention 验收(按 `v3.0_acceptance.md`)。 diff --git a/specs/mvp/v3.0/v3.0_summary.md b/specs/mvp/v3.0/v3.0_summary.md deleted file mode 100644 index c952025..0000000 --- a/specs/mvp/v3.0/v3.0_summary.md +++ /dev/null @@ -1,166 +0,0 @@ -# MVP v3.0 迭代总结(Ray + SFTPGo + API + WebUI) - -本文总结 v3.0 迭代最终落地的功能、架构、运行方式、验收点与已知限制,便于后续评审、交接与继续迭代。 - -相关更详细文档: -- `specs/mvp/v3.0/v3.0_design.md` -- `specs/mvp/v3.0/v3.0_api.md` -- `specs/mvp/v3.0/v3.0_dev_plan.md` -- `specs/mvp/v3.0/v3.0_acceptance.md` -- `specs/mvp/v3.0/v3.0_progress.md` - ---- - -## 1. 目标与范围 - -v3.0 作为“第一版可发布”的最小闭环,主要新增: -- **WebUI**:最小可用的人机界面(登录、任务提交与查看、数据入口、管理员入口)。 -- **用户管理**:基于内部 token 的用户体系(admin 与普通用户),支持创建用户与签发 token。 -- **数据管理入口(SFTPGo)**:用户通过 SFTP/WebClient 上传下载自己的数据;同时暴露只读的共享数据/缓存目录(common)用于复用。 -- **保持训练闭环**:仍通过 Ray Job 提交到集群执行(PPO/GRPO/SFT 三类 workload 都验证)。 - -明确不做(本迭代保持最小): -- 不支持用户自定义训练代码(TaskSpec 的 `code_path` 固定走 common 下的 verl snapshot 策略)。 -- 不做复杂资源排队优化/多集群/多租隔离策略(目前隔离粒度主要在用户 jobs 目录层)。 - ---- - -## 2. 系统架构(最终形态) - -核心组件: -- **Ray 集群(容器)** - - `argus-ray-head`:head 节点(无 GPU/不跑训练),提供 Ray Dashboard 与 Job Server。 - - `argus-ray-worker-0/1`:worker 节点(有 GPU),承载训练任务。 - - worker 以 “stateless + watchdog 自动连接 head” 的方式加入集群。 -- **API Server(运行在 head 容器内)** - - 读取 YAML 配置(dev/prod),维护任务队列(sqlite),并周期性调度将任务提交到 Ray。 - - 同时承载 WebUI(`/ui`)。 -- **SFTPGo(容器)** - - 提供 SFTP(端口 `2022`)与 Web Client/Admin(端口 `8081` 映射到容器 8080)。 - - 用户 home 为 `/private/users/`,默认可读写。 - - 额外提供 `/common/*` 共享只读入口(见第 4 节)。 -- **共享存储(NFS/GPFS 等挂载到容器内 `/private`)** - - `/private/common`:共享缓存(hf、datasets、models、db、logs 等)。 - - `/private/users/`:用户隔离目录(jobs/datasets/models/code/trash 等)。 - ---- - -## 3. 任务与调度(Task / Ray Job) - -### 3.1 Task(平台概念) -- 用户向 API 提交 TaskSpec(YAML),平台分配 `task_id`(可读、包含用户名)。 -- `task_id` 对应内部状态机与重试逻辑;底层每次提交 Ray Job 会产生 attempt 与 `ray_submission_id`。 - -### 3.2 Ray Job(Ray 概念) -- 真正执行训练的 driver 通过 Ray Job 运行在集群 worker 上(避免 head 承载训练)。 -- head 节点通过 `--num-cpus=0` / 自定义资源等策略避免调度到 head。 - -### 3.3 VERL 资源预检查的处理 -- VERL 在创建资源池时会做 fail-fast 资源预检查(如“可用 GPU 不足”直接报错退出)。 -- v3.0 延续 v2.x 的策略:服务端识别失败原因并按策略重试/回退(具体见 scheduler 实现与 v2.5/3.0 文档)。 - ---- - -## 4. 数据管理(SFTPGo)与 common 只读目录 - -### 4.1 用户目录(读写) -- 用户通过 SFTP/WebClient 访问自己的 home:`/private/users/` -- 目录结构(至少):`datasets/ models/ code/ jobs/ trash/ common/` - -### 4.2 common 只读(方案 A:Virtual Folder) -本迭代采用 SFTPGo 的 Virtual Folder + 路径权限覆盖,实现用户可读共享目录但不可写。 - -最终对外暴露为: -- `/common/datasets`(只读) - - **mapped_path 指向真实目录 `/private/datasets`**(避免 `/private/common/datasets` 中大量 symlink 导致的 WebClient “权限不足/越界”问题) -- `/common/hf`(只读) - - mapped_path 指向 `/private/hf` - -备注: -- `/private/common/datasets` 内部存在 symlink(如 `gsm8k -> /private/datasets/gsm8k`),如果虚拟目录映射到 symlink 根目录,SFTPGo 会把 symlink 跳转视为“逃逸 root”,导致点击进入时报权限不足;因此选择直接映射到真实目录根。 - ---- - -## 5. WebUI(最小可用) - -入口: -- `/ui/login`:粘贴 token(存 browser `localStorage`) -- `/ui/tasks`:任务列表(Running/Pending/Completed),Completed 支持分页 -- `/ui/tasks/new`:提交任务(PPO/GRPO/SFT 三套样例可一键填充) -- `/ui/data`:展示当前用户名、支持重置 SFTPGo 密码并复制;提供跳转到 SFTPGo WebClient;提示 FileZilla 等客户端用法 -- `/ui/admin`:管理员入口(创建用户、签发 token、用户列表) -- 导航栏提供 Ray Dashboard 快捷跳转(当前 IP 的 `:8265`) - -关于 admin 页面权限: -- admin 页面本身可访问,但其数据请求必须携带 admin token;否则会在页面内显示 401/403/错误信息(满足“需要先提供 admin token 才能看到内容”)。 - ---- - -## 6. API(v3.0 新增/强化点) - -核心接口(节选): -- 认证: - - Bearer token:`MVP_INTERNAL_TOKEN`(admin)或用户 token(由 admin 签发) -- 用户管理(admin): - - `POST /api/v2/users` 创建用户(并初始化用户目录) - - `GET /api/v2/users` 获取用户列表(包含最新 token、创建/更新时间等) - - `POST /api/v2/users/{user_id}/tokens` 签发用户 token -- 任务: - - `POST /api/v2/tasks` 提交 TaskSpec(YAML) - - `GET /api/v2/tasks` 任务列表(支持 states/limit/offset,用于 Completed 分页) - - `GET /api/v2/tasks/{task_id}`、`POST /api/v2/tasks/{task_id}:cancel`、`GET /api/v2/tasks/{task_id}/logs` - - `GET /api/v2/queue`(运行中/待调度概览) -- 数据/SFTP: - - `GET /api/v2/me` 返回用户路径信息、SFTP 连接信息,并 best-effort 对齐 SFTPGo 用户配置 - - `POST /api/v2/me/sftp:reset_password` 用户自助重置 SFTPGo 密码(一次性返回明文) - -安全取舍说明(当前为内网/开发优先): -- 为了 Admin WebUI “可查看并复制 token”,数据库持久化存储了 `token_plain`(明文 token)。 - - 这在生产场景通常不建议;未来可改为只展示“重置/重新签发”而不回显明文,或只回显一次。 - ---- - -## 7. 持久化与清理 - -- 任务队列:sqlite(WAL 模式) -- SFTPGo:自带 sqlite db(容器挂载持久化目录) -- Jobs 目录清理策略(服务端 janitor): - - job 结束后 3 天移动到回收目录(trash) - - 回收目录再保留 7 天后删除 - ---- - -## 8. 运行方式与脚本 - -开发/验收脚本: -- `src/mvp/scripts/run_all_v30_api.sh`:端到端拉起(Ray + SFTPGo + API),并通过 API 提交 PPO/GRPO/SFT,等待完成并验收 -- 其他脚本用于启动/停止 API、准备数据与模型、探测服务就绪等(详见 scripts 目录与 README) - -典型端到端(示例参数): -- `MVP_INTERNAL_TOKEN=my-dev-token` -- `SFTPGO_ADMIN_PASSWORD=my-dev-sftpgo-admin` -- 支持 `RESET_DB/RESET_SFTPGO` 用于测试环境重置 - ---- - -## 9. 验证结果(已跑通) - -在 `argus@h1` 环境中已完成端到端验证: -- Ray 集群可用(head + 2 worker) -- API server + WebUI 可用 -- SFTPGo(admin + 普通用户)可用 -- 通过 API 连续提交 PPO/GRPO/SFT 三种任务均能完成(SUCCEEDED) -- 用户可以登录 SFTPGo WebClient/SFTP,访问自己的目录,并访问 `/common/datasets`、`/common/hf` 的只读内容 - -同时本地单测通过: -- pytest 全绿 -- 覆盖率阈值 >= 90% - ---- - -## 10. 已知限制 & 后续可改进 - -- WebUI 当前为最小版,交互与权限提示仍偏“工程化”而非产品化(后续可增强错误提示、搜索筛选、任务详情聚合等)。 -- token 明文持久化仅适合内网/开发场景;生产建议改为一次性展示或支持撤销/轮换策略。 -- SFTPGo 虚拟目录目前保留了历史遗留映射(例如 `/common/models` 可能残留),后续可在升级脚本中做一次性清理与迁移。 - diff --git a/specs/mvp/v3.5/README.md b/specs/mvp/v3.5/README.md deleted file mode 100644 index c6df997..0000000 --- a/specs/mvp/v3.5/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# MVP v3.5 - -本目录包含 v3.5 的需求与设计(精简版): - -- `requirement.md`:需求补充说明(来源于讨论) -- `roadmap_v3.5.png`:架构草图(Advanced Task + Resume + IB + Serving) -- `v3.5_design.md`:详细设计方案(基于 v3.0;当前迭代仅聚焦 Advanced TaskSpec + Custom Reward,Serving/IB/Resume/多版本 verl 暂缓) diff --git a/specs/mvp/v3.5/note.md b/specs/mvp/v3.5/note.md deleted file mode 100644 index c68bdfd..0000000 --- a/specs/mvp/v3.5/note.md +++ /dev/null @@ -1,3 +0,0 @@ - -1. node management(v3.5 引入的接口骨架:通过 SSH/平台能力管理 head/worker 节点生命周期;先做最小可用 --- 这个是干嘛的? -2. \ No newline at end of file diff --git a/specs/mvp/v3.5/requirement.md b/specs/mvp/v3.5/requirement.md deleted file mode 100644 index 56520f6..0000000 --- a/specs/mvp/v3.5/requirement.md +++ /dev/null @@ -1,40 +0,0 @@ - -v3.5 版本是在v3.0的基础上进行功能扩展: -1. 支持自定义命令,不走固定的TaskSpec模板,用户直接提供调用verl 的python命令,如下,这个灵活度更高,需要用户自己把握文件路径,用户使用 $HOME,服务层替换为用户自己的/private/users//路径,使用$COMMON 则替换为/private/ - -``` -PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \ - data.train_files=$HOME/data/gsm8k/train.parquet \ - data.val_files=$HOME/data/gsm8k/test.parquet \ - data.train_batch_size=256 \ - data.max_prompt_length=512 \ - data.max_response_length=512 \ - actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \ - actor_rollout_ref.actor.optim.lr=1e-6 \ - actor_rollout_ref.actor.ppo_mini_batch_size=64 \ - actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \ - actor_rollout_ref.rollout.name=vllm \ - actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \ - actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ - actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ - actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \ - critic.optim.lr=1e-5 \ - critic.model.path=Qwen/Qwen2.5-0.5B-Instruct \ - critic.ppo_micro_batch_size_per_gpu=4 \ - algorithm.kl_ctrl.kl_coef=0.001 \ - trainer.logger=console \ - trainer.val_before_train=False \ - trainer.n_gpus_per_node=1 \ - trainer.nnodes=1 \ - trainer.save_freq=10 \ - trainer.test_freq=10 \ - trainer.total_epochs=15 -``` - -2. 支持自定义的奖励函数方法,你参考 verl 项目 [text](../../../verl) 里的示例,设计方案 - -3. 支持codepath指定用户上传到自己user路径下的 verl版本代码 - -4. 断点续训:支持某个已经complete(成功或者fail或者stopped)的任务task,从最后一个保存的checkpoint 继续训练,参数应该保持不变,你确认一下是不是对应一个新的ray job,或者分析一下verl 是否已经有类似的功能支持。 - -5. 支持训练走NCCL,使用RoCEv2和Infiband网络,调研一些verl怎样支持,需要哪些配置。 \ No newline at end of file diff --git a/specs/mvp/v3.5/roadmap_v3.5.png b/specs/mvp/v3.5/roadmap_v3.5.png deleted file mode 100644 index 779e05b..0000000 Binary files a/specs/mvp/v3.5/roadmap_v3.5.png and /dev/null differ diff --git a/specs/mvp/v3.5/v3.5_changes.md b/specs/mvp/v3.5/v3.5_changes.md deleted file mode 100644 index b67f58c..0000000 --- a/specs/mvp/v3.5/v3.5_changes.md +++ /dev/null @@ -1,67 +0,0 @@ -# MVP v3.5 功能变更总结(相对 v3.0) - -> v3.5 本轮按已确认的精简 scope:**只聚焦 Advanced TaskSpec + Custom Reward(方式 A:用户在 command 里写 overrides)**。Serving/IB/断点续训/多版本 verl 等能力本轮仍不做。 - -## 1. TaskSpec / 任务语义 - -### 1.1 新增 Advanced TaskSpec(自定义 command) - -- 新增 `kind: advanced` 的 TaskSpec 类型: - - 用户可提交任意 bash `command`,不再局限于平台内置 PPO/GRPO/SFT 模板。 - - `workload` 不再要求用户填写,也不做 infer;平台内部统一按 `"advanced"` 做任务分类与 task_id 命名(避免未来训练类型扩展带来的限制)。 -- 支持 `$HOME` 宏替换(服务端提交前展开): - - `$HOME` → `/private/users/` - - `$HOME/common/datasets` → `/private/datasets` - - `$HOME/common/hf` → `/private/hf` -- `command` 校验(best-effort,面向内部可信用户): - - 要求包含 `python3 -m verl.`(允许 `verl.trainer.*` / `verl.model_merger` 等)。 - - 不做强沙箱;主要防止明显误用导致的不可预期行为。 - -### 1.2 Custom Reward(方式 A) - -- 平台不新增 reward 专用字段、不扩展 TaskSpec schema。 -- 用户通过在 `command` 里写 VERL 原生 overrides 来注入 reward: - - `custom_reward_function.path=...` - - `custom_reward_function.name=...` - - `custom_reward_function.reward_kwargs=...`(如需) -- 平台侧仅做: - - 基础路径/宏展开($HOME) - - best-effort 的字符串校验(不做深度 AST 解析) - -## 2. WebUI(New Task 体验增强,仍兼容 YAML) - -- `New Task` 页面新增 **YAML 模式 / 表单模式**切换: - - 表单模式只覆盖 **5 个模板**:PPO / GRPO / SFT / Advanced / Model Merge。 - - 表单模式实时生成 YAML 预览;Submit 时提交生成 YAML;可一键切回 YAML 模式继续手工编辑。 -- `Advanced example`: - - 示例命令改为多行、可读性更好。 - - 补齐 PPO 常见 fail-fast 所需的关键 overrides(例如 actor micro batch),避免用户“照抄即失败”。 -- 新增 `Model merge example`(Advanced command 形式): - - 使用 `python3 -m verl.model_merger merge ...` - - 支持用 `$HOME/jobs//...` 访问训练产物目录。 - -## 3. SFTPGo / common 目录可读性(配合 v3.5 的 $HOME/common 语义) - -> 这些变更主要用于保证 v3.5 所定义的 `$HOME/common/{datasets,hf}` 语义在 SFTPGo WebClient/客户端下可用。 - -- `/common/datasets` 与 `/common/hf` 作为 SFTPGo virtual folders 暴露为只读共享目录: - - 允许 list + download(用于浏览与下载/查看内容;仍不允许 upload/rename/delete)。 - - 权限规则覆盖到子路径(避免“能进目录但文件不可读”的情况)。 -- API 调用 SFTPGo admin API 的连通性增强: - - dev 环境下避免依赖容器内 DNS(部分 head 容器环境存在临时解析失败),改为通过 docker bridge 网关 + 映射端口访问 admin API。 -- API 启动脚本确保注入 `SFTPGO_ADMIN_PASSWORD`(与 compose 默认值保持一致),避免 Reset Password 走到 401。 - -## 4. 兼容性与行为变化 - -- **完全兼容 v3.0 的 PPO/GRPO/SFT TaskSpec YAML**(原有字段与提交方式不变)。 -- 新增能力不会影响 ray/node management(仍按 v3.0:head 发布 discovery、worker watchdog join/self-heal)。 -- Advanced 任务不会进入 PPO/GRPO/SFT 的语义约束;平台仅负责: - - 资源字段(`nnodes` / `n_gpus_per_node`)用于队列调度与提交 gate - - 将 `command` 作为 Ray job entrypoint 执行 - -## 5. 已知限制(v3.5 不做) - -- 不提供“可视化” reward 配置面板(仅方式 A:用户自己写 command)。 -- 不支持 per-job 自定义 verl 代码快照/多版本共存(本轮不做 code_path 选择)。 -- 不支持断点续训一键 resubmit / IB(RDMA) / model serving(按 roadmap 后续版本推进)。 - diff --git a/specs/mvp/v3.5/v3.5_design.md b/specs/mvp/v3.5/v3.5_design.md deleted file mode 100644 index f85edbc..0000000 --- a/specs/mvp/v3.5/v3.5_design.md +++ /dev/null @@ -1,366 +0,0 @@ -# MVP v3.5 详细设计方案(进一步精简版,基于 v3.0) - -> 背景:v3.0 已具备 WebUI + API server + 用户/任务隔离 + SFTPGo 数据管理 + Stateless Ray cluster(head + worker node pool)。 -> -> v3.5 本轮 **只做 2 件事**: -> 1) Advanced Task:支持用户提交自定义训练命令(command) -> 2) Custom Reward:支持用户通过 VERL 原生 `custom_reward_function.*` 方式注入 reward(仅方式 A:用户自己写命令) -> -> 明确不做(从上一版设计中移除):(3) 自定义 verl 版本/代码路径、(4) 断点续训、(5) IB/RoCEv2 网络支持、(6) Model Serving。 - ---- - -## 0. 继承 v3.0 的不变点(重要约束) - -1) **Node management 不变** -- v3.5 不新增/不修改 node management 机制;仍按 v3.0 现状运行(head 写 discovery、worker watchdog 自动 join、自愈)。 - -2) **Head 不跑训练** -- 所有训练/Serving driver 通过 Ray entrypoint placement 强制落在 worker(例如 `entrypoint_resources={"worker_node": 1}`)。 - -3) **SFTPGo 的 “common” 目录约定变更** -- 不再使用 `$COMMON` 宏。 -- 在 SFTPGo 中,把共享只读资源映射到用户 home 下的固定目录(用户在 SFTP/WebClient 看到的是 `$HOME/common/...`): - - `$HOME/common/datasets` → 容器内真实路径 `/private/datasets`(只读) - - `$HOME/common/hf` → 容器内真实路径 `/private/hf`(只读) - -> 这里的 `$HOME` 指:`/private/users/`(容器内路径)。 - ---- - -## 1. v3.5 需求范围(精简后) - -### 1.1 In scope - -**A. Advanced TaskSpec(自定义命令)** -- 用户提交 `command`(多行 shell 或单行) -- 平台做 `$HOME` 宏替换 -- 平台做 best-effort 安全检查(路径/关键参数),然后提交为 Ray job - -**B. Custom Reward(仅方式 A)** -- 用户在 `command` 里显式写 hydra overrides: - - `custom_reward_function.path=...` - - `custom_reward_function.name=...` - - `custom_reward_function.reward_kwargs.*=...`(可选) -- 平台不提供结构化 reward 字段(不做方式 B),只做检查(校验 path 合法) - -### 1.2 Out of scope(本轮不做) -- 自定义 verl 版本/代码路径(仍使用平台内置/公共 verl 代码快照) -- 断点续训(resume from checkpoint) -- IB/RoCEv2 网络专门支持(NCCL/RDMA env 先不引入平台) -- Model Serving(暂缓,后续单独设计迭代) - ---- - -## 2. Advanced TaskSpec 设计 - -### 2.1 为什么需要 Advanced Task - -v3.0 的 Basic TaskSpec(ppo/grpo/sft)通过平台模板生成固定 overrides,适合“快速跑通”。 -但科研/调参场景需要更高自由度:用户希望直接写 `python3 -m verl.trainer.main_ppo ...` 并自行控制每个 override。 - -### 2.2 Advanced TaskSpec(建议 schema) - -建议新增一种 TaskSpec 类型,通过 `kind: advanced` 区分: - -```yaml -kind: advanced - -# 资源(平台调度与预检查用;仍需要) -nnodes: 2 -n_gpus_per_node: 4 - -# 自定义命令(用户负责写对 VERL 的参数/路径) -# 平台会对 $HOME 做宏替换;其余保持原样 -command: | - PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \ - data.train_files=$HOME/datasets/gsm8k/train.parquet \ - data.val_files=$HOME/datasets/gsm8k/test.parquet \ - actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \ - trainer.nnodes=2 \ - trainer.n_gpus_per_node=4 \ - trainer.total_epochs=1 \ - trainer.save_freq=10 \ - +ray_kwargs.ray_init.address=auto -``` - -### 2.3 `$HOME` 宏替换规则 - -仅支持 `$HOME`(v3.5 移除 `$COMMON`): -- `$HOME` → `/private/users/` - -用户如果要用共享数据/缓存: -- 共享数据:`$HOME/common/datasets/...` -- 共享 HF 缓存:`$HOME/common/hf/...`(通常不需要写进 command,但可用于 debug) - -#### 2.3.1 重要说明:SFTPGo “virtual folder” 与训练进程看到的“真实路径” - -在 SFTPGo 中,`$HOME/common/datasets` / `$HOME/common/hf` 是 **SFTP 虚拟目录映射**(virtual folder),它们映射到容器内真实路径: -- `$HOME/common/datasets` ↔ `/private/datasets` -- `$HOME/common/hf` ↔ `/private/hf` - -训练进程(Ray worker 上的 python 进程)看到的是 **容器内真实文件系统**,它并不会理解 SFTPGo 的 virtual folder。 - -因此,为了让用户能沿用 WebClient 里看到的路径语义(写 `$HOME/common/...`),服务层在提交 Advanced command 前需要做 **路径宏映射**: - -- `"$HOME/common/datasets"` → `"/private/datasets"` -- `"$HOME/common/hf"` → `"/private/hf"` -- 其余 `"$HOME"` → `"/private/users/"` - -这样用户写的 command 能在训练进程里正确读到文件。 - -### 2.4 服务层检查(best-effort,强约束 + 弱约束) - -> 目标:在不“解析完整 shell”的前提下,尽可能避免跨用户读文件与明显错误的任务。 - -**强约束(必须通过,否则 400)** -1) `nnodes`、`n_gpus_per_node` 必须存在(用于队列/资源预检查/placement) -2) `command` 必须包含一个明确的 python entry: - - 建议最低要求:包含 `python3` 且包含 `-m verl.trainer.`(防止随意执行系统命令) -3) 路径隔离校验(字符串/正则级别): - - 展开 `$HOME`(含 `$HOME/common/*` 映射到 `/private/*`)后: - - 禁止出现 `/private/users/` 下 “非当前用户”的路径(例如 `/private/users/bob/...`) - - 对 `data.train_files=...`、`data.val_files=...`(若出现)做 allowlist: - - 允许(用户目录):`/private/users//datasets/...` - - 允许(共享目录):`/private/datasets/...` - - 对 `custom_reward_function.path=...`(若出现)做 allowlist: - - 允许:`/private/users//code/...`(用户自行上传) - -**弱约束(warning,不阻塞)** -- 未检测到 `data.train_files=`/`data.val_files=`(可能是用户写成了别的 key 或使用了 config file) -- 未检测到 `+ray_kwargs.ray_init.address=auto`(v3.0/v3.5 推荐加,但用户可自行负责) - -> 说明:Advanced command 本质上属于“内部可信用户”能力,v3.5 不做强沙箱;安全检查以 best-effort 为主。 - ---- - -## 3. Custom Reward(仅方式 A:用户自己写) - -### 3.1 VERL 原生机制(本仓库 `verl/` 已调研) - -VERL PPO trainer 配置里支持: -- `custom_reward_function.path` -- `custom_reward_function.name` -- `custom_reward_function.reward_kwargs` - -对应实现位置: -- 配置模板:`verl/verl/trainer/config/ppo_trainer.yaml` -- 加载逻辑:`verl/verl/trainer/ppo/reward.py:get_custom_reward_fn` -- 典型 reward manager:`verl/verl/workers/reward_manager/naive.py` 会调用 `compute_score(...)` - -### 3.2 用户写法(示例) - -用户上传 `$HOME/code/reward.py`,在 command 里加: - -```bash -custom_reward_function.path=$HOME/code/reward.py \ -custom_reward_function.name=compute_score -``` - -函数签名建议(与 `naive` reward manager 参数对齐): - -```python -def compute_score(*, data_source: str, solution_str: str, ground_truth: str, extra_info=None, **kwargs): - ... -``` - -### 3.3 平台侧只做检查(不做字段扩展) - -v3.5 限定 reward 注入方式为 “用户写 command”,平台只做: -- 展开 `$HOME` -- 若检测到 `custom_reward_function.path=`,校验 path 在 `$HOME/code/` 下 -- 不尝试解析/合并 reward_kwargs(用户自己写) - ---- - -## 4. 服务层与 SFTPGo 的映射修改(你提出的关键点) - -v3.0 时代平台允许用户引用: -- `/private/common/datasets/...` -- `/private/common/hf/...` - -但现在 common 以 **SFTPGo virtual folder** 的形式呈现给用户(用户看到 `$HOME/common/...`,真实路径是 `/private/...`),因此 v3.5 的服务层需要做两件事: - -1) **用户侧语义(写 TaskSpec/command)** -- 共享 datasets(只读):`$HOME/common/datasets/...` -- 共享 hf cache(只读):`$HOME/common/hf/...` - -2) **运行时真实路径(提交到 Ray 前展开)** -- `$HOME/common/datasets/...` → `/private/datasets/...` -- `$HOME/common/hf/...` → `/private/hf/...` - -同时保留用户自有目录: -- 用户 datasets:`$HOME/datasets/...` -- 用户 models:`$HOME/models/...` -- 用户 code(reward):`$HOME/code/...` - -> 这部分主要影响: -> - Advanced command 检查(allowlist) -> - WebUI/Data 页面文案(告诉用户共享数据在哪里) - -> 兼容性建议:为了不影响 v3.0 期间已经习惯使用 `/private/common/datasets/...` 的用户/历史任务, -> v3.5 实现阶段建议 **同时接受**: -> - `/private/common/datasets/...`(旧路径语义,仍可读) -> - `/private/datasets/...`(真实路径语义,推荐) -> - Advanced command 里写的 `$HOME/common/datasets/...` 会先映射到 `/private/datasets/...` - ---- - -## 5. 验收标准(精简版) - -### 5.1 Advanced command -- 提交一个 Advanced PPO command(train/val 使用 `$HOME/common/datasets/...` 或 `$HOME/datasets/...`) -- 确认: - - 任务从 QUEUED → SUBMITTED/RUNNING - - driver 在 worker 上(head 不跑训练) - - 训练能正常跑至少若干 step - -### 5.2 Custom reward(方式 A) -- 用户上传 `$HOME/code/reward.py` -- 在 command 中设置 `custom_reward_function.path=$HOME/code/reward.py` -- 确认训练日志出现 `using customized reward function ...` - ---- - -## 6. 待确认问题(需要你拍板/补充) - -1) Advanced command 的“强约束”是否需要更严格? - - 目前建议要求包含 `python3 -m verl.trainer.`,否则拒绝。 - - 你是否允许用户跑非 verl 的命令(例如自定义评估脚本)? - -2) `$HOME/common/datasets` 与 `$HOME/common/hf` 两个映射目录在平台侧是否需要“强制只读”语义? - - 例如:TaskSpec 校验允许读取但禁止写入(目前设计是 best-effort 字符串级校验)。 - ---- - -## 7. 基于现有源码的改动点分析(实现清单) - -本节按当前 v3.0 已上线的源码结构(`src/mvp/py/argus/...`)逐文件列出 v3.5 需要的具体改动点,并评估对现有能力的影响面。 - -### 7.1 TaskSpec/模型层(解析与兼容) - -**现状** -- Basic TaskSpec 由 `argus.ray.models.JobSpec.from_dict()` 解析:`src/mvp/py/argus/ray/models.py` -- API `/api/v2/tasks` 直接 `JobSpec.from_dict(obj)`,并基于字段做路径校验:`src/mvp/py/argus/service/app.py` -- Scheduler 同样假定 jobspec_yaml 能解析为 `JobSpec`:`src/mvp/py/argus/service/scheduler.py` - -**v3.5 需要新增** -1) 新增 `AdvancedTaskSpec` 数据结构(建议放在 `src/mvp/py/argus/ray/models.py`): - - 必填:`kind: advanced`、`workload`(建议仍要求 ppo/grpo/sft,用于 task_id 命名与 UI 分类)、`nnodes`、`n_gpus_per_node`、`command` - - 可选:`submission_id`(由服务层 override) -2) 新增 “union 解析”: - - 新增 `parse_taskspec(obj: dict) -> Basic(JobSpec) | Advanced(AdvancedTaskSpec)` - - 兼容策略:如果没有 `kind` 字段,则 **默认按 v3.0 Basic JobSpec 解析**(保证老客户端无感)。 - -### 7.2 Builder 层(把 TaskSpec 转为可执行 argv) - -**现状** -- `src/mvp/py/argus/ray/builders.py:build_training_argv(spec: JobSpec, ...)` 只支持模板化 PPO/GRPO/SFT。 - -**v3.5 需要新增** -1) 新增 `build_advanced_argv(command: str) -> list[str]` - - 推荐实现:返回 `["bash", "-lc", ""]` - - 原因:用户 command 允许 `ENV=... python3 ... \` 多行以及 shell 语法,`bash -lc` 兼容性最好。 -2) Driver entrypoint 复用: - - 仍通过 `argus.ray.driver_entrypoint` 执行(统一 job_dir、日志与退出码)。 - -### 7.3 RayJobTool 层(runtime_env 与提交) - -**现状** -- `src/mvp/py/argus/ray/ray_job_tool.py:RayJobTool.submit(spec: JobSpec, ...)`: - - runtime_env 的 `PYTHONPATH` 由 `spec.code_path` 决定 - - entrypoint 固定为 driver_entrypoint + builder 生成 argv - -**v3.5 需要新增** -1) 扩展 submit 支持 AdvancedTaskSpec: - - 方案 A(最小侵入):新增 `submit_advanced(...)` 方法,参数为 `command` + `job_dir` + `submission_id` + `nnodes/n_gpus...` - - 方案 B(统一接口):新增内部抽象 `SubmitPlan`(包含 `runtime_env` + `entrypoint` + `artifacts`),Basic/Advanced 都生成 plan,再走同一 submit 逻辑。 -2) runtime_env 的 code path: - - 因 v3.5 本轮不做“自定义 verl code_path”,建议仍固定使用公共快照(例如 `/private/common/code/verl/verl_repo`)。 - - 为减少散落常量,建议在 config 增加 `ray.verl_code_path`(或 `service.verl_code_path`),RayJobTool 统一读取。 -3) runtime_env 的用户代码目录(可选增强): - - VERL 的自定义 reward 函数是通过 `custom_reward_function.path` 以“文件路径”动态 import 的,理论上不依赖 `PYTHONPATH`。 - - 但用户的 `reward.py` 可能会 `import` 自己目录下的其他模块;为了提升易用性,可将 - `/private/users//code` 追加到 job 的 `PYTHONPATH`。 - - 这需要 RayJobTool.submit/submit_advanced 能感知 `user_id`(由 Scheduler 传入),属于小改动但要注意兼容性。 - -### 7.4 API Server(提交校验、宏替换、spec 展示) - -**现状** -- `POST /api/v2/tasks`:只支持 Basic JobSpec 且强校验 `code_path/train_file/val_file/model_id` 前缀:`src/mvp/py/argus/service/app.py` -- `/api/v2/tasks/{task_id}/spec`:返回 resolved 的 Basic JobSpec(补默认值/补 submission_id):`src/mvp/py/argus/service/app.py` - -**v3.5 需要新增/修改** -1) `POST /api/v2/tasks` 分流: - - `kind != advanced`:走原 Basic 流程(兼容 v3.0) - - `kind == advanced`:走 Advanced 解析 + 校验 -2) Advanced command 宏替换与映射(核心): - - 实现 `expand_command(user_id, command)`: - - 先把 `$HOME/common/datasets` → `/private/datasets` - - 再把 `$HOME/common/hf` → `/private/hf` - - 再把其余 `$HOME` → `/private/users/` - - 校验使用 “展开后的 command” -3) reward 注入检查(仅方式 A): - - 若发现 `custom_reward_function.path=...`: - - 校验展开后的 path 前缀必须是 `/private/users//code/` -4) `/api/v2/tasks/{task_id}/spec`: - - 需要支持返回 AdvancedTaskSpec 的 resolved 版本: - - 展示时可选择“原始 command”(含 `$HOME`)或“展开后的 command”(建议都展示:raw + expanded) - -### 7.5 Scheduler(队列与提交) - -**现状** -- `src/mvp/py/argus/service/scheduler.py` 假定 jobspec_yaml 一定是 Basic JobSpec,并调用 `tool.submit(spec2, ...)`。 - -**v3.5 需要新增** -1) Scheduler 的 `_parse_jobspec` 替换为 `parse_taskspec`(支持 Basic/Advanced)。 -2) `_submit_one` 根据 spec 类型调用: - - Basic:保持现状 `tool.submit(JobSpec, ...)` - - Advanced:调用 `tool.submit_advanced(...)`(或统一 SubmitPlan) - -### 7.6 WebUI(最小改动) - -**现状** -- `src/mvp/py/argus/service/ui.py` 的 New Task 页面只提供 Basic YAML 模板。 - -**v3.5 需要新增** -- 增加 “Advanced Task” 模板按钮: - - `kind: advanced` - - `workload: ppo|grpo|sft`(用于 UI 分类与 task_id) - - `nnodes/n_gpus_per_node` - - `command: | ...`(带中文注释) -- Data 页面文案更新: - - 明确共享目录在 `$HOME/common/datasets`、`$HOME/common/hf`(并解释会映射到 `/private/datasets`、`/private/hf`) - ---- - -## 8. 对现有功能的兼容性影响评估 - -### 8.1 API/TaskSpec 兼容 -- 兼容策略:**没有 `kind` 字段的 YAML 一律按 v3.0 Basic JobSpec 解析**。 - - 现有脚本/客户端(提交 ppo/grpo/sft 的 YAML)无需修改。 -- AdvancedTaskSpec 是新增能力,不影响既有任务状态机/DB。 - -### 8.2 路径策略变更的影响 -风险点:v3.0 的 Basic 任务/模板大量使用 `/private/common/datasets/...`。 - -建议: -- v3.5 实现阶段先保持 “双栈兼容”: - - Basic 继续接受 `/private/common/datasets/...`(旧) - - 同时接受 `/private/datasets/...`(新/真实路径) -- Advanced command 允许用户写 `$HOME/common/datasets/...`,服务层展开为 `/private/datasets/...`(避免虚拟目录不可见问题)。 - -### 8.3 任务执行/调度兼容 -- Scheduler 队列/并发控制(`max_running_tasks`)保持不变。 -- 资源预检查仍只依赖 `nnodes/n_gpus_per_node`,AdvancedTaskSpec 不改变资源模型。 - -### 8.4 安全边界变化 -- Advanced command 引入后,平台从“结构化参数”变成“执行用户命令”,安全边界变宽。 -- 缓解措施(best-effort): - - 强约束要求命令包含 `python3 -m verl.trainer.` - - 基础路径隔离校验(禁止跨用户路径) - - reward 文件路径限制在 `$HOME/code` - -### 8.5 数据库兼容 -- DB schema 不强制变更:仍复用 `tasks.jobspec_yaml` 存储原始 YAML。 -- 若后续需要更强查询/过滤,再考虑增加 `tasks.kind` 字段(可选增量迁移)。 diff --git a/specs/mvp/v3.5/v3.5_dev_plan.md b/specs/mvp/v3.5/v3.5_dev_plan.md deleted file mode 100644 index b05682c..0000000 --- a/specs/mvp/v3.5/v3.5_dev_plan.md +++ /dev/null @@ -1,200 +0,0 @@ -# MVP v3.5(精简版)开发计划(TDD) - -> 目标:在 v3.0 已有能力基础上,仅新增两项能力: -> 1) **Advanced TaskSpec(自定义 command)** -> 2) **Custom Reward(方式 A:用户自己在 command 里写 `custom_reward_function.*`)** -> -> 设计依据:`specs/mvp/v3.5/v3.5_design.md`(本计划不再扩展 scope)。 - ---- - -## 0. 范围与约束 - -### 0.1 In scope -- 新增 `kind: advanced` 的 TaskSpec:用户提供 `command`,平台做 `$HOME` 宏替换与 best-effort 校验,再提交 Ray Job。 -- Custom Reward:平台仅做 **reward path 校验**(方式 A),不新增结构化字段。 -- `$HOME/common/*` 路径语义支持(关键):用户在 SFTPGo/WebClient 看到的路径能被训练进程正确读取。 - -### 0.2 Out of scope(本轮不做) -- 自定义 verl 版本/代码路径(多版本共存) -- 断点续训(resume from checkpoint) -- IB/RoCEv2/NCCL 专项支持 -- Model Serving -- Node management 改造(v3.0 的 stateless head/worker/watchdog/supervisor 机制保持不变) - -### 0.3 关键路径映射(必须保持一致) -> 说明:SFTPGo 的 `$HOME/common/...` 是 **virtual folder**,训练进程看不到该虚拟路径。 - -提交 Advanced command 前必须展开/映射: -- `$HOME/common/datasets` → `/private/datasets`(只读语义) -- `$HOME/common/hf` → `/private/hf`(只读语义) -- 其余 `$HOME` → `/private/users/` - -并且为兼容历史用法(v3.0): -- Basic TaskSpec 仍接受 `/private/common/datasets/...`、`/private/common/hf/...`(不强制迁移)。 - ---- - -## 1. 测试策略(TDD) - -### 1.1 单元测试优先级 -1) **解析与兼容**:`kind: advanced` 能解析;无 `kind` 仍按 Basic 解析,旧用法不破坏。 -2) **宏替换正确性**:`$HOME` / `$HOME/common/*` 映射严格按约定展开。 -3) **best-effort 校验**:拒绝明显危险/跨用户路径;对 reward path 做 allowlist。 -4) **提交链路**:Scheduler 能识别 Advanced spec 并调用对应的提交方法,确保 submission_id/目录规范不变。 -5) **WebUI/API**:New Task 模板与 `/spec` 展示完整 resolved spec(包含展开后的 command)。 - -### 1.2 本地运行方式 -- 复用已有 `.venv`,执行:`.venv/bin/python -m pytest` -- 若环境没有 pip,使用 uv 的方式参考 v3.0 约定(不在本计划重复)。 - ---- - -## 2. 里程碑划分(每个里程碑可独立验证) - -> 约定:每个里程碑先写测试(失败),再实现代码使测试通过;里程碑结束跑一遍 `pytest`。 - -### M1 — TaskSpec 模型与解析(兼容优先) -**目标** -- 引入 AdvancedTaskSpec 数据结构与 union parser,同时保证 v3.0 Basic 行为不变。 - -**新增/修改(建议位置)** -- `src/mvp/py/argus/ray/models.py` - - 新增 `AdvancedTaskSpec` - - 新增 `parse_taskspec(obj: dict) -> JobSpec | AdvancedTaskSpec` - - 兼容策略:缺省 `kind` → 走 `JobSpec.from_dict` - -**测试(先写)** -- `src/mvp/py/tests/test_models.py` - - `test_parse_taskspec_basic_no_kind_compat()` - - `test_parse_taskspec_advanced_smoke()` - - `test_parse_taskspec_advanced_requires_command_nnodes_gpus()` - -**验收** -- `pytest -q` 通过;旧测试不修改或仅做最小必要更新。 - ---- - -### M2 — Advanced command 展开与校验(核心能力) -**目标** -- 实现 command 展开(含 `$HOME/common/*` 映射)与 best-effort 强约束校验。 - -**实现点(建议新增模块)** -- `src/mvp/py/argus/service/command_expand.py`(或放在 `argus/service/validation.py`) - - `expand_advanced_command(user_id: str, command: str) -> str` - - `validate_advanced_command(user_id: str, expanded_command: str) -> None`(失败抛 `ValueError`) - -**强约束(与设计文档一致)** -- 必须包含 `python3` 且包含 `-m verl.trainer.`(否则 400) -- 禁止出现 `/private/users//...`(跨用户路径) -- 若检测到 `data.train_files=`/`data.val_files=`: - - 只允许 `/private/users//datasets/...` 或 `/private/datasets/...` - - (兼容)允许 `/private/common/datasets/...`(旧路径) -- 若检测到 `custom_reward_function.path=`: - - 只允许 `/private/users//code/...`(展开后校验) - -**测试(先写)** -- 新增:`src/mvp/py/tests/test_advanced_command.py` - - `test_expand_maps_home_common_datasets_to_private_datasets()` - - `test_expand_maps_home_common_hf_to_private_hf()` - - `test_expand_maps_home_to_private_users()` - - `test_validate_rejects_cross_user_paths()` - - `test_validate_requires_verl_trainer_entry()` - - `test_validate_allows_reward_path_under_user_code()` - - `test_validate_rejects_reward_path_outside_user_code()` - -**验收** -- 单测覆盖映射/校验的正反例;错误信息可读(用于 API 400 detail)。 - ---- - -### M3 — Ray 提交链路支持 Advanced(Builder/Tool/Scheduler) -**目标** -- Advanced spec 能进入 scheduler 队列并提交为 Ray job(driver 仍落 worker)。 - -**代码改动点(建议)** -- `src/mvp/py/argus/ray/builders.py` - - 新增 `build_advanced_argv(command: str)`:返回 `["bash","-lc", expanded_command]` -- `src/mvp/py/argus/ray/ray_job_tool.py` - - 新增 `submit_advanced(...)`(或统一成内部 submit plan) - - runtime_env:继续注入公共 verl code path(本轮不支持用户自定义 verl 代码) - - 可选:把 `/private/users//code` 加入 `PYTHONPATH`,提升 reward 代码 `import` 体验 -- `src/mvp/py/argus/service/scheduler.py` - - 使用 `parse_taskspec` 分流 Basic/Advanced - - Advanced 调用 `tool.submit_advanced(...)` - -**测试(先写)** -- `src/mvp/py/tests/test_builders.py` - - `test_build_advanced_argv_uses_bash_lc()` -- `src/mvp/py/tests/test_scheduler.py` - - 新增一个 `kind: advanced` 的任务,断言 scheduler 调用了 `submit_advanced` - - 断言 job_dir/submission_id 规则不变(仍按 `/private/users//jobs/`) -- `src/mvp/py/tests/test_ray_job_tool.py` - - 断言 advanced 提交时 entrypoint 是 driver_entrypoint + `bash -lc ...` - -**验收** -- 单测跑通;Scheduler tick 能完成 Advanced 任务从 QUEUED → SUBMITTED(mock Ray)。 - ---- - -### M4 — API & WebUI(最小功能闭环) -**目标** -- WebUI/HTTP API 能提交 Advanced Task,并在详情页看到 resolved spec(含完整 command)。 - -**API 改动点** -- `src/mvp/py/argus/service/app.py` - - `POST /api/v2/tasks`:支持 `kind: advanced` - - 保存 raw YAML(保持与 Basic 一致) - - 对 Advanced:展开 command + 校验(失败返回 400) - - `GET /api/v2/tasks/{task_id}/spec`: - - 返回 resolved spec(建议同时返回 raw + expanded,或 YAML 中直接给 expanded) - -**WebUI 改动点** -- `src/mvp/py/argus/service/ui.py` - - New Task 页面新增 Advanced 模板(含中文注释) - - 文案强调共享目录:`$HOME/common/datasets`、`$HOME/common/hf` - -**测试(先写)** -- `src/mvp/py/tests/test_app.py` - - `test_create_task_advanced_ok()`(最小 valid command) - - `test_create_task_advanced_rejects_invalid_command()` - - `test_task_spec_endpoint_includes_expanded_command()` -- `src/mvp/py/tests/test_ui.py` - - 断言页面包含 Advanced 示例块 - -**验收** -- `pytest` 通过;浏览器可提交 Advanced YAML 并看到 expanded command。 - ---- - -### M5 — 端到端验证(远端 argus@h1) -**目标** -- 在真实 Ray cluster + VERL 环境下验证 Advanced 与 Custom Reward(方式 A)。 - -**步骤(手工验收脚本化可选)** -1) 启动 v3.0/v3.5 统一的 compose + API(沿用现有 `run_all` 脚本体系) -2) 用户(如 `alice`)通过 SFTP 上传 reward 代码到: - - `$HOME/code/reward.py`(真实路径 `/private/users/alice/code/reward.py`) -3) 通过 WebUI 或 curl 提交 Advanced task: - - `command` 中包含: - - `custom_reward_function.path=$HOME/code/reward.py` - - `custom_reward_function.name=compute_score` - - `data.train_files=$HOME/common/datasets/gsm8k/train.parquet` - - `data.val_files=$HOME/common/datasets/gsm8k/test.parquet` -4) 检查: - - 任务状态从 QUEUED → RUNNING → SUCCEEDED/FAILED(有日志) - - driver 不在 head 上跑(dashboard 验证) - - 日志出现 “custom reward” 生效的提示(按 VERL 实际日志关键字确认) -5) 回归:提交 Basic ppo/grpo/sft 任务仍可运行(确保兼容性) - -**验收** -- Advanced task 能跑至少若干 step,且 reward 注入生效。 -- Basic 任务兼容不回退。 - ---- - -## 3. 风险点与边界(明确写进 PR/变更说明) -- Advanced command 只做 best-effort 校验,不做完整 shell AST 解析;复杂命令可能存在漏检/误判(后续可扩展)。 -- `$HOME/common/*` 是“用户侧语义”,服务层必须映射到真实路径,否则训练必然 FileNotFound。 -- 校验策略(强约束)如果后续要允许非 VERL 命令,需要调整规则并补测试(本轮默认拒绝)。 - diff --git a/specs/mvp/v3.6/README.md b/specs/mvp/v3.6/README.md deleted file mode 100644 index fc220f4..0000000 --- a/specs/mvp/v3.6/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# MVP v3.6 - -本目录包含 v3.6 的需求与设计: - -- `Snipaste_2026-01-05_10-56-34.png`:v3.6 架构草图(在 v3.5 基础上增加 Weights & Biases;其余模块保持不变) -- `requirements.md`:需求要点(W&B + Evaluation 模板) -- `wandb.md`:W&B local server 的前期调研与资料(license、部署方式、VERL 配置要点等) -- `v3.6_design.md`:v3.6 详细设计方案(基于 v3.5) -- `v3.6_progress.md`:v3.6 里程碑进度记录 diff --git a/specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png b/specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png deleted file mode 100644 index 8e889e2..0000000 Binary files a/specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png and /dev/null differ diff --git a/specs/mvp/v3.6/requirements.md b/specs/mvp/v3.6/requirements.md deleted file mode 100644 index 34d8e08..0000000 --- a/specs/mvp/v3.6/requirements.md +++ /dev/null @@ -1,3 +0,0 @@ -1. 增加wandb功能 -2. 增加evaluation 模板 - diff --git a/specs/mvp/v3.6/v3.6_design.md b/specs/mvp/v3.6/v3.6_design.md deleted file mode 100644 index 2af592a..0000000 --- a/specs/mvp/v3.6/v3.6_design.md +++ /dev/null @@ -1,280 +0,0 @@ -# MVP v3.6 详细设计方案(基于 v3.5) - -> 设计基线:当前线上已具备 v3.5 能力(PPO/GRPO/SFT + Advanced TaskSpec + SFTPGo 数据管理 + WebUI)。 -> v3.6 的架构草图见:`specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png` - -## 0. 目标与范围 - -### 0.1 v3.6 目标 - -1) **Weights & Biases(W&B)集成** -- 训练/任务运行时自动打点到 W&B local server。 -- 采用“共享 W&B 账号(license 只支持 1 user) + 为每个 MVP 用户创建独立 project”的方式隔离可视化与检索体验。 -- 平台提供 W&B 的跳转链接、以及每个 task 对应 run 的可定位信息(最小闭环)。 - -2) **New Task 增加 Evaluation 模板** -- 在 New Task 页面提供一个最小可用的 “Evaluation” 任务模板(用于离线评估/打分),并能把结果落到 job 输出与(可选)W&B。 - -### 0.2 非目标(v3.6 不做) - -- 不引入新的 node management 机制;保持 v3.5 的 head discovery + worker watchdog stateless pool。 -- 不做 Serving/IB/RDMA/断点续训/多版本 verl code_path 等(仍按 v3.5 的范围)。 -- 不做多租户安全隔离(W&B API Key 注入属于内部可信环境)。 - ---- - -## 1. W&B local server(部署与连通) - -> 资料参考:`specs/mvp/v3.6/wandb.md`。**文档中 license/token 属敏感信息,v3.6 设计不在代码/文档里写死。** - -### 1.1 部署方式(dev/h1) - -建议在 `src/mvp/docker-compose.yaml` 新增 `wandb` 服务(与现有 ray_head / sftpgo 同一 compose network): - -- 镜像:`wandb/local:latest` -- 容器端口:`8080` -- 宿主机端口:建议 `8090:8080`(避免和 MVP API `8080`、SFTPGo `8081` 冲突) -- 持久化:挂载到 NFS/共享目录(例如 `/private/common/wandb`)以持久化 runs/artifacts -- 首次启动后由管理员在 W&B System Admin 页面粘贴 license(见 `wandb.md`) - -### 1.1.1 持久化策略(必须) - -v3.6 约定 **W&B server 的元数据必须持久化**(否则会丢 license/账号/API key、历史 runs/project 索引等): - -- W&B server 数据目录(`/vol`)挂载到共享目录(NFS):例如 `/private/common/wandb:/vol` -- 这部分数据由平台/管理员长期保留(不跟随单个 job 清理) - -与之相对,**每个 Ray job 对应的本地 W&B run 文件**放在 job 目录下(见 §2.3 的 `WANDB_DIR`),并由现有 janitor 随 job 一起清理: - -- `WANDB_DIR=/private/users//jobs//wandb` -- janitor 对 job 的策略是“结束后 3 天移入回收站、7 天后删除”,因此该目录会被一并处理 - -> 说明:W&B 的最终“事实数据”在 server 侧持久化(runs/metrics/可视化);job 目录下的 `WANDB_DIR` 更像运行期缓存/临时文件与调试材料。 - -### 1.2 容器内访问 W&B 的 base_url - -在 v3.5 经验中,Ray head 容器对 docker 内部 DNS 名称解析不稳定(`Temporary failure in name resolution`)。 -为避免再次踩坑,v3.6 统一采用 **“docker bridge 网关 + host 映射端口”** 的方式让容器访问 W&B: - -- `WANDB_BASE_URL=http://172.22.0.1:8090`(其中 `172.22.0.1` 为 `mvp_argus-ray-net` 网关) - -> 注意:如果未来 dev 环境 network 网段变化,需要把网关地址做成配置项(见 §2)。 - ---- - -## 2. 平台侧 W&B 集成(API/Scheduler/Ray Job runtime_env) - -### 2.1 配置设计(YAML) - -在 `configs/dev.yaml`(以及生产配置)中新增一段 W&B 配置,建议结构: - -```yaml -tracking: - wandb: - enabled: true - base_url: "http://172.22.0.1:8090" - api_key_env: "WANDB_API_KEY" - entity: "" # 可选;verL 通过 env WANDB_ENTITY 读取 - project_suffix: "_project" # 例如 alice_project - # 可选:代理(verL 支持 trainer.wandb_proxy) - proxy: null -``` - -平台读取 `api_key_env` 对应的环境变量,并在 job 维度注入 `WANDB_API_KEY`。 -**不在配置文件中存明文 API KEY**(避免泄露)。 - -### 2.2 项目/命名规范(共享账号 + user project) - -由于 W&B local license 限制 “最多 1 user”,v3.6 采用: - -- **同一个 W&B 账号**(同一个 `WANDB_API_KEY`) -- 每个 MVP user 使用不同 project: - - `project_name = + project_suffix` - - 示例:`alice_project` -- 每个 Ray job attempt 对应一个 run: - - `experiment_name = `(保证 attempt 唯一;也便于从 dashboard 反查) - -verL 内部 `wandb.init(project=project_name, name=experiment_name, entity=$WANDB_ENTITY)`(见 `verl/utils/tracking.py`)。 - -### 2.3 Ray Job 注入(runtime_env env_vars) - -当 `tracking.wandb.enabled=true` 时,在 scheduler 提交 ray job 时,统一注入: - -- `WANDB_BASE_URL`(来自配置) -- `WANDB_API_KEY`(来自 env `tracking.wandb.api_key_env`) -- `WANDB_ENTITY`(可选) -- `WANDB_MODE=online`(默认在线;可在 dev/离线时切换) -- `WANDB_DIR=/private/users//jobs//wandb`(保证每个 job 的本地 run 文件落到对应 job 目录;便于随 job 一起被 janitor 清理) - -> 对 Advanced TaskSpec:平台无法替用户改 command,但仍可注入上述 env,让用户在 command 中自行启用 wandb。 - -### 2.4 verL 训练任务自动开启 wandb logger - -对平台内置 workload(PPO/GRPO/SFT),平台将 hydra overrides 改为: - -- `trainer.logger=[console,wandb]`(替代 v3.5 的 `trainer.logger=console`) -- `trainer.project_name=_project` -- `trainer.experiment_name=` -- 可选:如果配置了 `tracking.wandb.proxy`,注入 `trainer.wandb_proxy=...` - -兼容性说明: -- `trainer.logger` 在 verL 的 ppo/sft config 中本身是 list(示例为 `["console","wandb"]`),因此不会破坏 verL 配置解析。 -- 现有 v3.5 的 checkpoint/log dir 仍按 job_dir 注入,不依赖 `trainer.default_local_dir`。 - -### 2.5 数据库存储与 API 输出(最小闭环) - -v3.6 需要让用户能在 WebUI/Task 详情中 “点一下跳转到 W&B”: - -建议在 DB(sqlite)里新增 attempt 级字段(或 metadata JSON): - -- `wandb_project`:如 `alice_project` -- `wandb_run_name`:如 `` -- `wandb_base_url`:如 `http://:8090` -- `wandb_url`:拼出来的最终链接(若 W&B local 的 URL 结构不稳定,可只存前 3 项,由前端拼接) - -API 侧在: -- `GET /api/v2/tasks/` 的 `latest_attempt` 增加 `wandb` 字段(或顶层增加 `tracking` 字段) -- `GET /api/v2/me` 增加 `wandb` 信息(base_url、project_name),便于页面展示 “Open W&B” - -> W&B local 的 URL 路径结构需要在接入时确认;若不确定,先只提供 base_url + project/run 名称,用户可在 W&B UI 搜索。 - ---- - -## 3. WebUI 变更(v3.6) - -### 3.1 Data / Login 页 - -- 在 Data 或 Login 页新增: - - “Open W&B” 链接(跳转到 `tracking.wandb.base_url` 的 Web UI) - - 展示当前用户的 project 名称(例如 `alice_project`),并提供 Copy 按钮 - -### 3.2 Tasks / Task Detail - -- Task list 无需大改;Task detail 页面增加: - - W&B run 信息(project / run name / link) - - 若任务失败,W&B 仍可用于查看已上报的中间日志(对训练类任务有价值) - -### 3.3 New Task 增加 Evaluation 模板 - -New Task 页面新增一个 “Evaluation example”: - -#### 方案 A(推荐,最小改动):作为 Advanced TaskSpec 模板 - -- 直接提供 `kind: advanced` 的模板,command 运行 verL 自带评估入口: - - `python3 -m verl.trainer.main_eval data.path=... custom_reward_function.path=... +ray_kwargs.ray_init.address=auto` -- 优点:不需要扩展 TaskSpec schema / scheduler 逻辑 -- 缺点:Evaluation 在平台侧不可结构化(仍属于 advanced) - -#### 方案 B(更产品化):新增内置 workload: evaluation(后续可选) - -若希望在任务分类/队列中把 evaluation 作为一类“内置任务”,可新增: - -```yaml -workload: evaluation -code_path: /private/common/code/verl/verl_repo -nnodes: 1 -n_gpus_per_node: 0 -data_path: $HOME/common/datasets/<...>.parquet -response_key: responses -data_source_key: data_source -reward_model_key: reward_model -custom_reward_path: $HOME/code/reward.py # 可选 -custom_reward_name: compute_score # 可选 -``` - -平台把它编译成 `verl.trainer.main_eval` 的 hydra overrides + ray init address;并可选择把结果解析后上报 W&B。 - -v3.6 建议先落地 **方案 A**(模板即可),方案 B 作为 v3.7+ 的演进。 - ---- - -## 4. 验收标准(v3.6) - -### 4.1 W&B(训练任务) - -- 提交 PPO/GRPO/SFT 任一任务: - - W&B local server 能看到对应 project(如 `alice_project`) - - 能看到 run 名称为该任务 attempt 的 `ray_submission_id` - - run 内能持续刷新 metrics(至少包含 loss/step 等 verL 默认上报) -- WebUI: - - 能打开 W&B UI 链接 - - Task detail 能展示 project/run(或至少可检索信息) - -### 4.2 Evaluation 模板 - -- New Task 中出现 “Evaluation example” -- 复制模板提交后: - - 任务能在 Ray 上运行(CPU 即可,`+ray_kwargs.ray_init.address=auto` 连接集群) - - 输出 metrics(`main_eval.py` 默认 print dict) - - (可选)若用户在 command 里启用 wandb,则能在对应 project 下看到评估 run - ---- - -## 5. 兼容性影响评估 - -- 对现有 v3.5 功能: - - 默认行为改变:PPO/GRPO/SFT 会默认多一个 logger(wandb),并对外发起 HTTP 请求到 W&B server。 - - 若 W&B server 不可用/配置缺失: - - 建议行为:平台自动降级为 `trainer.logger=[console]` 并在 task 状态中给出 warning(避免训练直接失败)。 - - 初版也可选择 fail-fast:缺少 `WANDB_API_KEY` 时拒绝开启 wandb(由配置开关控制)。 -- 对资源/存储: - - W&B server 自身会写入一定量数据(license 限制 10GB);需配合 retention 策略做清理(v3.6 先手动,后续可自动)。 - - job 目录下的 `WANDB_DIR` 会随 jobs retention 被清理;不会无限增长。 - ---- - -## 6. 已确认的落地约定 - -- W&B server 对外端口:`8090` -- Project 命名:`_project`(不使用 `WANDB_ENTITY`) -- Evaluation:先只提供 **Advanced 模板**(New Task 页面提供示例即可) - ---- - -## 7. 运维与验收流程(dev/h1) - -### 7.1 启动服务(docker compose) - -1) 启动/重启 compose(Ray head/worker + SFTPGo + W&B): - -```bash -cd /home2/argus/infra/mvp/src/mvp -docker compose up -d -``` - -2) 访问 UI: -- MVP WebUI:`http://:8080/ui` -- Ray Dashboard:`http://:8265` -- SFTPGo Web:`http://:8081/web/` -- W&B Web:`http://:8090` - -> W&B 容器数据目录已挂载到共享盘:`/private/common/wandb`(容器内 `/vol`)。 - -### 7.2 初始化 W&B(管理员一次性操作) - -1) 打开 `http://:8090/system-admin` 粘贴 license(详见 `specs/mvp/v3.6/wandb.md`)。 -2) 进入 W&B UI 创建/登录到共享账号(license 只支持 1 user)。 -3) 在 W&B UI 的用户设置页面生成 API Key(通常位于 “User Settings / API Keys”)。 - -### 7.3 启动 API server(需要注入 WANDB_API_KEY) - -平台不会把 `WANDB_API_KEY` 写入配置文件;必须在启动 API server 时通过环境变量提供。 - -示例(在宿主机): - -```bash -export MVP_INTERNAL_TOKEN="my-dev-token" -export WANDB_API_KEY="...从 W&B UI 复制..." -cd /home2/argus/infra/mvp/src/mvp/scripts -./60_start_api.sh -``` - -> 说明:`./60_start_api.sh` 会把 `WANDB_API_KEY` 透传给 head 容器内运行的 API server。 - -### 7.4 验收(最小闭环) - -1) 在 WebUI Login 页面能看到 W&B 区块(Open W&B + project copy)。 -2) 提交一个 PPO/GRPO/SFT 任务(任意一个即可): - - W&B project 为 `_project`(如 `alice_project`) - - run name 为该 attempt 的 `ray_submission_id` -3) 提交 Evaluation 模板(Advanced)能在 Ray 上运行并输出评估结果(stdout / logs)。 diff --git a/specs/mvp/v3.6/v3.6_dev_plan.md b/specs/mvp/v3.6/v3.6_dev_plan.md deleted file mode 100644 index d0fa1e3..0000000 --- a/specs/mvp/v3.6/v3.6_dev_plan.md +++ /dev/null @@ -1,194 +0,0 @@ -# MVP v3.6(基于 v3.5)开发计划(TDD) - -> 设计依据:`specs/mvp/v3.6/v3.6_design.md` -> 本计划默认已确认: -> 1) W&B host 端口:`8090`;2) project:`_project`;3) evaluation 先做 Advanced 模板;4) 不使用 `WANDB_ENTITY`。 - -## 总体原则 - -- **TDD**:每个里程碑先补齐单测(或契约测试)再实现功能;保证覆盖率门槛不回退。 -- **最小闭环**:先做到“可用+可验证”,再做体验优化;不做超出 v3.6 scope 的扩展。 -- **配置不落盘敏感信息**:`WANDB_API_KEY` 只能来自运行环境变量,不写入仓库配置文件。 - ---- - -## Milestones - -### M1:配置层(tracking.wandb)与解析 - -**目标** -- 在服务配置中新增 `tracking.wandb`,支持开关、base_url、api_key_env。 -- 不引入 `WANDB_ENTITY`(保持为空即可)。 - -**开发任务** -- 在 `src/mvp/py/argus/service/config.py`: - - 新增 `TrackingConfig/WandbConfig` dataclass(或等价结构)。 - - `V2Config.from_root_dict()` 解析 `tracking.wandb.*`(缺省为 disabled)。 - - 校验:`enabled=true` 时 `base_url` 不能为空;`api_key_env` 默认 `WANDB_API_KEY`。 - -**测试(先写)** -- `test_config_parses_wandb_defaults`:没有 tracking 字段时,默认 disabled。 -- `test_config_parses_wandb_enabled`:enabled=true 能读到 base_url/api_key_env。 -- `test_config_rejects_empty_base_url_when_enabled`:enabled=true 且 base_url 空时报错(或记录 warning,取决于实现选择)。 - -**验收** -- 仅通过 config 即能决定是否启用 W&B,且不会破坏 v3.5 现有配置解析。 - ---- - -### M2:Ray Job runtime_env 注入(WANDB_* env) - -**目标** -- 当 `tracking.wandb.enabled=true` 时:平台在 job 粒度注入 `WANDB_BASE_URL/WANDB_API_KEY/WANDB_MODE/WANDB_DIR` 等 env。 -- `WANDB_API_KEY` 从 API server 进程环境变量中读取:`os.environ[api_key_env]`。 - -**开发任务** -- 在 scheduler / ray job builder(`src/mvp/py/argus/service/scheduler.py` 或 `src/mvp/py/argus/ray/builders.py`): - - 构造 job runtime_env.env_vars 的 merge 逻辑: - - 现有 `ray.runtime_env.env_vars` 为基础; - - 追加 W&B env(不覆盖用户显式指定的同名变量,或按“平台优先”策略二选一并写清楚)。 - - 注入: - - `WANDB_BASE_URL=` - - `WANDB_API_KEY=` - - `WANDB_MODE=online` - - `WANDB_DIR=/private/users//jobs//wandb`(本地 run 文件随 job 一起由 janitor 清理) - -**测试(先写)** -- `test_scheduler_injects_wandb_env_when_enabled`: - - mock env 中存在 `WANDB_API_KEY`; - - 提交一个内置任务(ppo/sft 任意),断言构造出来的 runtime_env 含以上 env_vars。 -- `test_scheduler_sets_wandb_dir_under_job_dir`: - - 断言 `WANDB_DIR` 位于该 attempt 的 job 目录下(而不是 common 目录),避免无法跟随 job retention 清理。 -- `test_scheduler_does_not_inject_wandb_env_when_disabled`。 -- `test_scheduler_wandb_missing_api_key_behaviour`: - - enabled=true 但缺少 env 时的行为: - - 方案 A(推荐):**自动降级**为 console(不注入 wandb),并在 task/attempt message 记录 warning; - - 或方案 B:fail-fast(返回 500/400)。 - - 需在实现前确认采用哪种策略;建议 v3.6 选 A 提升可用性。 - -**验收** -- 任意内置任务提交后,在 Ray job runtime_env 中能看到 `WANDB_*`。 - ---- - -### M3:内置训练任务自动开启 wandb logger(PPO/GRPO/SFT) - -**目标** -- 当 W&B enabled 时,平台默认把内置训练任务改成 `trainer.logger=["console","wandb"]`,并设置 project/run 命名。 - -**开发任务** -- 在 job 构建(PPO/GRPO/SFT 的 overrides 生成处): - - 将 `trainer.logger` 从 `console` 改为 list:`[console,wandb]`(hydra 语法按现有实现方式拼接)。 - - `trainer.project_name=_project` - - `trainer.experiment_name=` - - 保持 v3.5 的 job_dir/checkpoint/log dir 注入方式不变。 - -**测试(先写)** -- `test_job_overrides_include_wandb_logger_when_enabled`: - - 断言 entrypoint/overrides 包含 `trainer.logger=[console,wandb]`(或等价写法)。 - - 断言包含 `trainer.project_name=_project`、`trainer.experiment_name=`。 -- `test_job_overrides_keep_console_only_when_wandb_disabled_or_missing_key`。 - -**验收** -- 训练任务 run 会自动出现在 W&B 对应 project 下(E2E 验证在 M6)。 - ---- - -### M4:API 输出与 WebUI 链接(最小闭环) - -**目标** -- 用户可以在 UI 里“知道去哪看 W&B”,以及知道本 task 对应哪个 project/run。 - -**开发任务** -- API: - - `GET /api/v2/me` 增加 `wandb` 信息(仅当 enabled 时返回): - - `base_url` - - `project_name`(`_project`) - - `GET /api/v2/tasks/{task_id}`(或 attempt 结构)增加 `wandb_project` / `wandb_run_name`(run_name=ray_submission_id)。 -- WebUI: - - Login/Data 页增加 “Open W&B” 链接(跳 `base_url`),展示 project_name + Copy。 - - Task detail 增加 wandb 字段展示(project/run/可点击链接或可复制文本)。 - -**测试(先写)** -- `test_me_includes_wandb_when_enabled`(mock config + env)。 -- `test_task_detail_contains_wandb_fields_when_enabled`(mock task/attempt)。 -- `test_ui_contains_wandb_link`(渲染 HTML 断言包含 base_url/project_name 字样)。 - -**验收** -- WebUI 能一键跳转 W&B;Task detail 能定位到 run。 - ---- - -### M5:New Task 增加 Evaluation 模板(Advanced) - -**目标** -- New Task 页面增加一个 Evaluation 模板按钮/示例(先按 Advanced TaskSpec 提供)。 - -**开发任务** -- 在 `src/mvp/py/argus/service/ui.py`: - - YAML 模式增加 “Evaluation example”。 - - 表单模式本轮可选(不要求): - - 如果要支持:把 evaluation 作为 advanced 模板的一种预填 command(仍 `kind: advanced`)。 -- 模板建议使用 verL 自带入口: - - `python3 -m verl.trainer.main_eval ... +ray_kwargs.ray_init.address=auto` - - `data.path=$HOME/common/datasets/...`(按 v3.5 的宏规则) - -**测试(先写)** -- `test_ui_new_task_contains_evaluation_example`:断言页面包含 `main_eval` 与关键字段。 - -**验收** -- 用户复制 evaluation 模板可提交成功并在 Ray 上运行(E2E 在 M6)。 - ---- - -### M6:端到端(dev/h1)部署与验收流程 - -> 里程碑 M6 以脚本/手工步骤为主;不强制写 e2e 自动化测试。 - -**部署任务** -- compose 增加 `wandb` 服务: - - `wandb/local:latest` - - host 端口 `8090:8080` - - 数据挂载到 `/private/common/wandb:/vol` 持久化(W&B 元数据/账号/API key/历史 runs) -- API 启动方式: - - 在宿主机 export:`WANDB_API_KEY=<从 W&B UI 生成的 key>` - - 启动 API(确保 env 透传到容器内) -- 首次初始化: - - 打开 `http://:8090/system-admin` 粘贴 license(管理员操作) - -**验收用例** -1) **训练类任务自动打点** - - 用 alice 提交一个 SFT(或 PPO)任务(内置 workload) - - 在 W&B UI 中看到 project:`alice_project` - - run name 为 `ray_submission_id`,metrics 可见 -2) **Advanced task 可手动打点** - - 提交一个 Advanced(用户自己写 command)并在 command 中启用 `trainer.logger=["console","wandb"]`(如需) - - 确认 env 注入生效(W&B 记录出现) -3) **Evaluation 模板** - - 用 New Task 的 Evaluation example 提交 - - 任务成功运行并输出 metrics(stdout/logs) - - (可选)如果 evaluation 也启用 wandb,则能出现在对应 project 下 - -**回归** -- v3.5 的三类任务(ppo/grpo/sft)在 W&B disabled 或缺少 key 时仍可跑通(至少 console 输出不受影响)。 - -**Retention 联动检查** -- 提交一个短任务生成 `WANDB_DIR`,结束后确认: - - `WANDB_DIR` 位于 `/private/users//jobs//wandb` - - janitor 运行后该目录会随 job 一起进入 trash / 被 purge(与 jobs retention 一致) - ---- - -## 交付物清单(v3.6) - -- 文档: - - `specs/mvp/v3.6/v3.6_design.md`(已存在,必要时补充操作流程) - - `specs/mvp/v3.6/v3.6_dev_plan.md`(本文) -- 代码(预期变更点): - - `src/mvp/py/argus/service/config.py` - - `src/mvp/py/argus/service/scheduler.py` / `src/mvp/py/argus/ray/builders.py` / `src/mvp/py/argus/ray/ray_job_tool.py` - - `src/mvp/py/argus/service/app.py`(/me 与 task detail 输出) - - `src/mvp/py/argus/service/ui.py`(Open W&B + Evaluation template) - - `src/mvp/docker-compose.yaml`(wandb service) - - `src/mvp/configs/dev.yaml`(tracking.wandb 配置) - - `src/mvp/scripts/*`(API 启动时 env 透传,必要时补充) diff --git a/specs/mvp/v3.6/v3.6_progress.md b/specs/mvp/v3.6/v3.6_progress.md deleted file mode 100644 index 7f681c8..0000000 --- a/specs/mvp/v3.6/v3.6_progress.md +++ /dev/null @@ -1,42 +0,0 @@ -# MVP v3.6 进度记录 - -> 基线:v3.5 已完成(Advanced TaskSpec + Custom reward(方式A)+ WebUI + SFTPGo + stateless ray node pool)。 -> 本文件用于记录 v3.6 每个 milestone 的完成情况与关键改动点。 - -## M1(完成) - -- 新增 `tracking.wandb` 配置解析与校验(enabled/base_url/api_key_env)。 - -## M2(完成) - -- Ray job 维度注入 `WANDB_*` env(含 `WANDB_BASE_URL/WANDB_API_KEY/WANDB_MODE/WANDB_DIR`),缺少 key 时降级并记录 warning。 - -## M3(完成) - -- PPO/GRPO/SFT 内置训练任务在 wandb 可用时自动追加 overrides: - - `trainer.logger=[console,wandb]` - - `trainer.project_name=_project` - - `trainer.experiment_name=` - -## M4(完成) - -- API 输出增加 W&B 定位信息: - - `/api/v2/me` 返回 `wandb.{enabled,base_url,project_name}` - - `/api/v2/tasks/{task_id}` 在 `latest_attempt.wandb` 返回 `{base_url,project_name,run_name}` -- WebUI: - - Login 页面增加 W&B 区块(跳转 8090、copy project) - - Task detail 页面增加 W&B 区块(copy run) - -## M5(完成) - -- WebUI New Task 增加 Evaluation 模板(Advanced): - - 使用 `python3 -m verl.trainer.main_eval ... +ray_kwargs.ray_init.address=auto` - - 以占位符路径示例(用户替换 `/`) - -## M6(完成) - -- `docker-compose.yaml` 集成 W&B local server: - - host 端口 `8090` - - 持久化目录 `/private/common/wandb`(容器内 `/vol`) -- dev 配置新增 `tracking.wandb` 默认开启(缺 key 自动降级并记录 warning)。 -- API 启动脚本支持把 `WANDB_API_KEY` 从宿主机透传到 head 容器中的 API server。 diff --git a/specs/mvp/v3.6/v3.6_summary.md b/specs/mvp/v3.6/v3.6_summary.md deleted file mode 100644 index 818bb4d..0000000 --- a/specs/mvp/v3.6/v3.6_summary.md +++ /dev/null @@ -1,126 +0,0 @@ -# MVP v3.6 迭代研发总结(基于 v3.5) - -> 时间基线:2026-01(H20 dev 环境:`argus@h1:/home2/argus/infra/mvp`) -> v3.6 架构草图:`specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png` - -## 1. 迭代目标回顾 - -v3.6 在 v3.5(WebUI + API server + Ray stateless node pool + SFTPGo + Advanced TaskSpec)基础上,新增两块能力: - -1) **Weights & Biases(W&B)local server 集成** -- 训练任务(PPO/GRPO/SFT)默认可写入 W&B。 -- 采用“共享 W&B 账号 + 按用户拆分 project(`_project`)”的隔离策略。 - -2) **New Task 增加 Evaluation 示例** -- New Task 页面新增一个最小可用的 evaluation 模板(以 Advanced command 方式运行 `verl.trainer.main_eval`)。 - -## 2. 交付内容(代码/配置/脚本) - -### 2.1 部署形态(docker compose) - -v3.6 在 `src/mvp/docker-compose.yaml` 新增 W&B 服务: - -- 服务名:`wandb`(容器名:`argus-wandb`) -- 宿主机端口:`8090:8080` -- 持久化:`../../shared/common/wandb:/vol` -- 同 network:`argus-ray-net`(便于 Ray 容器内访问) - -### 2.2 平台配置(YAML) - -在 `src/mvp/configs/dev.yaml` 增加/启用 W&B 配置: - -```yaml -tracking: - wandb: - enabled: true - base_url: "http://172.22.0.1:8090" - api_key_env: "WANDB_API_KEY" - project_suffix: "_project" -``` - -说明: -- `base_url` 采用 docker bridge 网关 + 宿主机映射端口的方式,规避容器内 DNS 偶发解析失败问题。 -- 不在 config 中写明文 key,统一通过启动 API server 时注入 `WANDB_API_KEY`。 - -### 2.3 Ray Job runtime_env 注入(核心) - -v3.6 在**每个 Ray job attempt**提交时注入两类环境变量: - -1) **始终注入(无论是否启用 W&B)**:便于 Advanced command 在不改模板的情况下能运行 -- `MVP_TRAINER_LOGGER`:`console` 或 `[console,wandb]` -- `MVP_WANDB_PROJECT`:`_project`(例如 `alice_project`) -- `MVP_WANDB_RUN`:``(每次 attempt 唯一) - -2) **当 W&B 有效启用时注入** -- `WANDB_BASE_URL` -- `WANDB_API_KEY` -- `WANDB_MODE=online` -- `WANDB_DIR=/wandb`(例如 `/private/users/alice/jobs//wandb`) - -降级策略: -- 当 `tracking.wandb.enabled=true` 但缺少 `WANDB_API_KEY` 时,平台会**降级为 console**(并在 attempt.message 中记录 warning),避免训练失败。 - -### 2.4 WebUI 变更 - -1) **Login 页面** -- 增加 “Open W&B” 跳转(指向 `tracking.wandb.base_url`) - -2) **New Task 页面** -- 新增 **Evaluation example** -- Advanced example 更新为 v3.6: - - `command: |` 内不再包含任何注释(避免 YAML/命令解析报错) - - W&B 参数不再让用户手填,改为引用平台注入的 `${MVP_*}` env: - - `trainer.logger=${MVP_TRAINER_LOGGER}` - - `trainer.project_name=${MVP_WANDB_PROJECT}` - - `trainer.experiment_name=${MVP_WANDB_RUN}` - -> 备注:driver 日志里会打印 `MVP_DRIVER_EXEC: bash -lc '...'`,此处看到 `${MVP_*}` 仍是“未替换”的字符串是正常现象;变量替换发生在 `bash` 执行阶段,而不是打印 argv 阶段。 - -### 2.5 启动脚本 - -`src/mvp/scripts/60_start_api.sh` 支持将宿主机的 `WANDB_API_KEY` 透传进 head 容器内启动的 API server: - -- 宿主机设置:`export WANDB_API_KEY=...` -- 启动 API:脚本会 `docker exec -e WANDB_API_KEY=...` 进入 head 容器启动 `python3 /workspace/mvp/py/server.py` - -## 3. 用户侧操作流程(v3.6) - -### 3.1 一次性初始化(只在首次启用/清空 /vol 时需要) - -1) 打开 W&B UI:`http://:8090` -2) 在 System Admin 页面粘贴 license 完成初始化 -3) 生成并记录 `WANDB_API_KEY`(local key) -4) 以后启动 API server 时注入该 key(`WANDB_API_KEY=...`) - -只要保留 `shared/common/wandb`(即 `/vol` 持久化目录),重建容器无需再次进入 8090 配置。 - -### 3.2 日常使用 - -1) WebUI 登录:`http://:8080/ui/login`(输入 user token) -2) New Task 提交任务:`http://:8080/ui/tasks/new` -3) 到 Tasks 查看状态/日志:`/ui/tasks` 与 task detail -4) 打开 W&B:`http://:8090`,在 `_project` 下查看 runs/metrics - -## 4. 验收结果(本迭代应达成) - -1) PPO/GRPO/SFT 任一任务运行后: -- W&B local server 可见对应 project(如 `alice_project`) -- run name 与 `ray_submission_id` 对齐(便于追踪每次 attempt) - -2) Evaluation 示例: -- 可作为 Advanced 任务提交并在 Ray 上执行 `verl.trainer.main_eval` -- 支持用户在 command 内自行加入 reward overrides(平台不做封装) - -## 5. 已知限制与后续建议 - -1) **W&B 初始化自动化** -- 当前:首次仍需在 8090 页面粘贴 license、生成 key(更稳、侵入最小)。 -- 若需要“从零部署也完全免页面操作”,可进一步调研 W&B local 的可用管理 API/启动参数(自动注入 license + 自动创建 key)。 - -2) **Advanced command 的自由度** -- 平台只负责: - - `$HOME` 宏替换 - - runtime_env env_vars 注入 - - 任务队列与 Ray job 提交 -- command 的语义正确性仍由用户负责(例如 PPO 必需的 micro batch 等参数)。 - diff --git a/specs/mvp/v3.6/wandb.md b/specs/mvp/v3.6/wandb.md deleted file mode 100644 index c7ddeda..0000000 --- a/specs/mvp/v3.6/wandb.md +++ /dev/null @@ -1,70 +0,0 @@ - -# License - -License: -eyJhbGciOiJSUzI1NiIsImtpZCI6InUzaHgyQjQyQWhEUXM1M0xQY09yNnZhaTdoSlduYnF1bTRZTlZWd1VwSWM9In0.eyJjb25jdXJyZW50QWdlbnRzIjoxLCJ0cmlhbCI6ZmFsc2UsIm1heFN0b3JhZ2VHYiI6MTAsIm1heFRlYW1zIjowLCJtYXhVc2VycyI6MSwibWF4Vmlld09ubHlVc2VycyI6MCwibWF4UmVnaXN0ZXJlZE1vZGVscyI6MiwiZXhwaXJlc0F0IjoiMjAyNy0wMS0wNVQwMjoxMjo1MC4zMjRaIiwiZGVwbG95bWVudElkIjoiYzNmN2Y5N2ItMzAxOS00Nzk2LTkxYTgtZDUyMjc1NDBiMTI1IiwiZmxhZ3MiOltdLCJjb250cmFjdFN0YXJ0RGF0ZSI6IjIwMjYtMDEtMDVUMDI6MTI6NTAuMzI0WiIsImFjY2Vzc0tleSI6IjYxMGM5NjliLTk4ZWEtNGRhNS1iYzU1LWM2MzVlZWNhNzc0OCIsInNlYXRzIjoxLCJ2aWV3T25seVNlYXRzIjowLCJ0ZWFtcyI6MCwicmVnaXN0ZXJlZE1vZGVscyI6Miwic3RvcmFnZUdpZ3MiOjEwLCJleHAiOjE3OTkxMTUxNzAsIndlYXZlTGltaXRzIjp7IndlYXZlTGltaXRCeXRlcyI6bnVsbCwid2VhdmVPdmVyYWdlQ29zdENlbnRzIjowLCJ3ZWF2ZU92ZXJhZ2VVbml0IjoiTUIifX0.VADnc0PExWhGDAxMIbu0vlmPN423B398of4HFl6BMJ1vqGA9H1ESElOZfk0VQ0YnYgwZc_CZF9k0HRyfCBgRhtRKyB1PpGnaKT_kKNVQryykWRpNhnpDqhmTa-wfTUBXNxhu1ktNPKBFNaEbaYuPsLN_aXPGW0dDwp6coGnGEXEqdRmuvekE6ytu7t6IA6flYs35WqCojvvjAmfBdovo2zPTfmlqKeaz7GPrApMo9JBpmb1a6bZEjCoRhhUx_k-v2rbvE3hd9ix9_UMZ6siJ5IKtNuXy_cprcCXXIFVUMcfTnt78RRXY0jCRMQqWkNq9ZGF0Mgcjsh3ts9xSxPgWnw - -# License 限制 -Add License to your Local Instance -Create up to 0 teams -Create up to 1 users -Store up to 10 GB of data -Create up to 2 Registered Models - -Quickstart -On a machine with Docker and Python installed, run: -1 pip install wandb --upgrade -2 wandb server start -Generate a free license from the Deployer. -Add it to your W&B Server's localhost's settings. -Paste the license in the /system-admin page on your localhost - -# docker 部署 - -deployment: -version: "3.8" - -services: - wandb: - image: wandb/local:latest - container_name: wandb-local - ports: - - "8080:8080" - volumes: - - wandb_data:/vol - restart: unless-stopped - -volumes: - wandb_data: - - - -# 连接方式: -方式 B:环境变量(适合容器/批处理/CI) - -通过 ray job的runtime_env来设置环境变量 - -export WANDB_BASE_URL=http://<服务器IP或域名>:8080 -export WANDB_API_KEY=<你的API_KEY> - - -官方文档说明可以用 WANDB_BASE_URL + WANDB_API_KEY 代替 wandb login --host .. - - -# verl配置: -在 verl 里打开 wandb(你只需要配 trainer) - -verl 的配置里,最关键是这三个字段:trainer.logger、trainer.project_name、trainer.experiment_name。文档里也写了 logger 用于 console + tracking(tracking 会初始化 wandb)。 -veRL Documentation -+1 - -推荐写法(新版本): - -trainer: - logger: ["console", "wandb"] - project_name: my_project # 用argus的用户名_project ,譬如 alice_project - experiment_name: exp_001 # 用 task id 作为实验名 - - - - diff --git a/specs/mvp/v3.7/v3.7_design.md b/specs/mvp/v3.7/v3.7_design.md deleted file mode 100644 index 436aed9..0000000 --- a/specs/mvp/v3.7/v3.7_design.md +++ /dev/null @@ -1,215 +0,0 @@ -# MVP v3.7 设计方案:切换 `verlai/verl:vllm011.latest` + 默认 rollout=vllm - -## 0. 背景与目标 - -当前 dev/h1 环境的 Ray 节点镜像基于 `verlai/verl:sgl055.latest`,并且平台内置 PPO/GRPO 的默认参数中写死了: - -- `actor_rollout_ref.rollout.name=sglang` - -v3.7 的目标是: - -1. **Ray 节点镜像切换到 vLLM 版本** - - 基础镜像改为 `verlai/verl:vllm011.latest` - - 构建并打标:`argus/argus-ray-node:vllm011.latest` - - 构建在远端 `argus@h1` 上完成(本地没有 verlai 基础镜像) -2. **端到端跑通 v3.0 API 流程** - - 通过 `src/mvp/scripts/run_all_v30_api.sh` 完整 E2E -3. **内置训练任务默认使用 vLLM rollout** - - 提交 VERL 训练任务时将 `actor_rollout_ref.rollout.name` 从 `sglang` 改为 `vllm` - -> 备注:本迭代是“替换默认 backend”而非“新增能力”,尽量保持对 v3.6 功能兼容(W&B、SFTPGo、Advanced TaskSpec、stateless pool 等不改协议)。 - ---- - -## 1. 现状梳理(源码定位) - -### 1.1 Ray 节点镜像与 compose - -- Dockerfile:`src/mvp/images/argus-ray-node/Dockerfile` - - 当前 `ARG BASE_IMAGE=verlai/verl:sgl055.latest` -- Compose:`src/mvp/docker-compose.yaml` - - `ray_head.build.args.BASE_IMAGE: verlai/verl:sgl055.latest` - - `ray_head.image / worker.image: argus/argus-ray-node:v2.5` - -### 1.2 默认 rollout.name=sglang 的位置 - -平台内置 PPO/GRPO 参数由 Ray job 入口构建器生成: - -- `src/mvp/py/argus/ray/builders.py` - - `build_training_argv()` 中写死了: - - `actor_rollout_ref.rollout.name=sglang` - -WebUI 的 Advanced 示例也包含 rollout.name(用于指导用户): - -- `src/mvp/py/argus/service/ui.py` - - Advanced example 中当前为 `actor_rollout_ref.rollout.name=sglang`(需要同步改成 vllm,避免用户 copy/paste 走错) - -### 1.3 `run_all_v30_api.sh` 依赖默认参数 - -`src/mvp/scripts/run_all_v30_api.sh` 提交 PPO/GRPO/SFT 的 TaskSpec(YAML)时 **不会显式携带 rollout.name**,因此是否能切到 vllm,依赖平台默认值(builders)是否变更。 - ---- - -## 2. 方案设计 - -### 2.0 已确认决策(来自评审) - -1) **compose 移除 build**:允许移除 `ray_head.build`,强制使用远端已构建镜像。 -2) **全量切换 vllm**:不保留 sglang 作为可选项(v3.7 默认全部切到 vllm)。 -3) **backend 名称**:确认 VERL backend 名为 `vllm`(即 `actor_rollout_ref.rollout.name=vllm`)。 - -### 2.1 镜像策略(vllm011) - -#### 2.1.1 Dockerfile 修改 - -目标: -- 默认基础镜像改为 `verlai/verl:vllm011.latest` - -改动点: -- `src/mvp/images/argus-ray-node/Dockerfile` - - `ARG BASE_IMAGE=verlai/verl:vllm011.latest` - -说明: -- 仍保留 `BASE_IMAGE` build arg,便于未来热切换不同基础镜像(而不是把镜像写死在 compose)。 - -#### 2.1.2 镜像 tag - -构建产物镜像: -- `argus/argus-ray-node:vllm011.latest` - -> 注意:该 tag 用于表达“运行时依赖的 vllm 版本线”,而不是 MVP 功能版本(v3.7)。 - -#### 2.1.3 compose 复用新镜像(避免每次重建) - -目标:E2E 时尽量避免每次 `docker compose up` 都 build。 - -建议修改 `src/mvp/docker-compose.yaml`: -- `ray_head.image: argus/argus-ray-node:vllm011.latest` -- `ray_worker_0.image: argus/argus-ray-node:vllm011.latest` -- `ray_worker_1.image: argus/argus-ray-node:vllm011.latest` - -并采用:**移除 `ray_head.build`**(强制使用已构建镜像),避免每次 `docker compose up` 触发 build。 - ---- - -### 2.2 训练默认参数切换到 vllm - -目标:平台内置 PPO/GRPO 的默认 rollout backend 从 sglang 切到 vllm。 - -改动点: -- `src/mvp/py/argus/ray/builders.py` - - 将 `actor_rollout_ref.rollout.name=sglang` 替换为 `actor_rollout_ref.rollout.name=vllm` - -影响范围: -- PPO、GRPO(两者都走 `verl.trainer.main_ppo`) -- 对 SFT 不影响(SFT 走 `verl.trainer.sft_trainer_ray`) - -兼容性评估: -- `run_all_v30_api.sh` 会受益:无需修改 TaskSpec,即可自动切换。 -- 若未来仍需支持 sglang,可考虑在 v3.7 之后引入“配置驱动”的默认值(见 §2.4 可选增强)。 - ---- - -### 2.3 WebUI/模板同步(避免误导用户) - -目标:New Task 页面的 Advanced example 也应默认 vllm,避免用户 copy 后手工改参数。 - -改动点: -- `src/mvp/py/argus/service/ui.py` - - Advanced example 中 `actor_rollout_ref.rollout.name=vllm` - -> 注意:该模板仅用于 UX 指导;实际生效仍由用户提交的 command 决定。 - ---- - -### 2.4 可选增强(不强制,供评审) - -为避免后续再硬编码切换,可引入“平台训练默认值”配置(可选): - -- 在 `configs/dev.yaml` 增加: - ```yaml - verl_defaults: - rollout_backend: "vllm" # 或 "sglang" - ``` -- `builders.py` 从配置读取默认值,而非写死。 - -本次 v3.7 的最低交付可以先不做该增强,只做硬替换;若你希望后续支持 A/B 切换,再纳入。 - ---- - -## 3. 远端部署/迁移步骤(argus@h1) - -> 本节是“计划步骤”,评审通过后再执行。 - -### 3.1 同步代码到远端目录 - -远端目录约定: -- `argus@h1:/home2/argus/infra/mvp/src/mvp`(compose 与 scripts) - -将本地变更 rsync 到远端后再进行构建/拉起。 - -### 3.2 在远端构建镜像(只在 h1) - -在 `argus@h1` 执行(示例命令): - -```bash -cd /home2/argus/infra/mvp/src/mvp -docker build \ - -f images/argus-ray-node/Dockerfile \ - --build-arg BASE_IMAGE=verlai/verl:vllm011.latest \ - -t argus/argus-ray-node:vllm011.latest \ - . -``` - -### 3.3 清理旧环境并用新镜像拉起 - -```bash -cd /home2/argus/infra/mvp/src/mvp -docker compose down -docker compose up -d -``` - -验证: -- `docker ps` 中 `argus-ray-head/worker` 的 image 为 `argus/argus-ray-node:vllm011.latest` -- Ray dashboard 可访问:`http://:8265` - -### 3.4 E2E:跑 `run_all_v30_api.sh` - -```bash -cd /home2/argus/infra/mvp/src/mvp -MVP_INTERNAL_TOKEN=my-dev-token \ -WANDB_API_KEY=... \ -./scripts/run_all_v30_api.sh -``` - -验收关键点: -- PPO/GRPO/SFT 全部成功(或至少 PPO/GRPO 不卡在 rollout backend 初始化阶段) -- 任一 PPO/GRPO 的 driver logs / hydra overrides 中能看到: - - `actor_rollout_ref.rollout.name=vllm` - ---- - -## 4. 风险与排查要点 - -### 4.1 vLLM backend 在 VERL 的参数兼容性 - -平台默认传入的这些参数当前是为 sglang 写的: -- `actor_rollout_ref.rollout.tensor_model_parallel_size=1` -- `actor_rollout_ref.rollout.gpu_memory_utilization=0.4` - -vLLM rollout 是否接受/需要额外参数(例如 tokenizer、engine 配置),需要在 E2E 中观察: -- 如果 vLLM rollout 初始化报错,可能需要补充 vllm 特定 overrides(属于 v3.7 的后续修复项)。 - -### 4.2 镜像依赖差异 - -更换 base image 可能带来: -- Python/Ray/依赖版本差异 -- CUDA/NCCL 依赖差异 - -建议: -- 在 v3.7 评审通过后,优先跑最小 PPO(epochs=1、steps=10)验证 vllm backend 能启动并完成。 - ---- - -## 5. 待确认问题(请你评审时确认) -已完成评审确认(见 §2.0),无额外待确认项。 diff --git a/specs/mvp/v3.7/v3.7_dev_plan.md b/specs/mvp/v3.7/v3.7_dev_plan.md deleted file mode 100644 index 61b0f58..0000000 --- a/specs/mvp/v3.7/v3.7_dev_plan.md +++ /dev/null @@ -1,122 +0,0 @@ -# MVP v3.7 开发计划(TDD) - -> 目标:切换 Ray 节点基础镜像到 `verlai/verl:vllm011.latest`,并将平台内置 PPO/GRPO 默认 rollout backend 全量切到 `vllm`,最后在远端 `argus@h1` 通过 `run_all_v30_api.sh` 跑通端到端。 - -## M0 - 基线确认(不改行为) - -**目的**:确认当前 v3.6 baseline 可跑(避免把历史问题混入 v3.7)。 - -- [ ] 本地单测全绿:`.venv/bin/python -m pytest` -- [ ] 远端 h1 当前环境可跑(可选):`./scripts/run_all_v30_api.sh`(或至少能启动 Ray+API) - -**验收**: -- 单测通过,coverage ≥ 90%(现有门槛) - ---- - -## M1 - 训练默认参数切换到 vllm(TDD) - -**目的**:在不碰镜像/compose 的前提下,先把“默认 rollout=sglang”替换为 vllm,并用单测锁定行为。 - -### 1.1 新增/更新单测(先写测试) - -- [ ] `src/mvp/py/tests/test_builders.py` - - 新增断言:PPO/GRPO 的 argv 中包含 `actor_rollout_ref.rollout.name=vllm` - - 且不再包含 `actor_rollout_ref.rollout.name=sglang` - -- [ ] `src/mvp/py/tests/test_ui.py` - - New Task Advanced example 模板包含 `actor_rollout_ref.rollout.name=vllm`(避免用户 copy/paste 走错默认) - -> 这两条测试先写出来,预期先失败(red)。 - -### 1.2 实现改动(让测试变绿) - -- [ ] `src/mvp/py/argus/ray/builders.py` - - 将 `actor_rollout_ref.rollout.name=sglang` 改为 `...=vllm` - -- [ ] `src/mvp/py/argus/service/ui.py` - - Advanced example 中同样改为 `...=vllm` - -### 1.3 回归测试 - -- [ ] `.venv/bin/python -m pytest` - -**验收**: -- 单测全绿(coverage ≥ 90%) -- 平台内置 PPO/GRPO 构建出的 command/overrides 默认 rollout backend 为 vllm - ---- - -## M2 - 镜像与 compose 切换(远端构建为主) - -**目的**:完成镜像切换与环境拉起,确保 Ray stateless pool 正常工作。 - -### 2.1 Dockerfile 默认 base image 切换 - -- [ ] `src/mvp/images/argus-ray-node/Dockerfile` - - `ARG BASE_IMAGE=verlai/verl:vllm011.latest` - -### 2.2 docker-compose 强制使用新镜像(移除 build) - -- [ ] `src/mvp/docker-compose.yaml` - - 移除 `ray_head.build` 段(强制走 `image:`) - - `ray_head.image / ray_worker_0.image / ray_worker_1.image` 统一改为: - - `argus/argus-ray-node:vllm011.latest` - -### 2.3 远端构建镜像(h1) - -在 `argus@h1:/home2/argus/infra/mvp/src/mvp`: - -- [ ] `docker build -f images/argus-ray-node/Dockerfile -t argus/argus-ray-node:vllm011.latest .` - -### 2.4 清理旧 compose 并拉起 - -- [ ] `docker compose down` -- [ ] `docker compose up -d` -- [ ] 验证: - - `docker ps` 看到 `argus-ray-head/worker` 正常运行 - - Ray dashboard:`http://:8265` 可访问,节点数 1 head + 2 worker - -**验收**: -- h1 环境成功使用新镜像拉起 Ray 集群(head 无 GPU、worker 各 4 GPU 的配置仍保持) - ---- - -## M3 - 端到端验证(run_all_v30_api.sh) - -**目的**:验证在新镜像 + 默认 vllm rollout 下,API 提交的训练任务能跑通闭环。 - -### 3.1 同步代码到远端 - -- [ ] rsync `src/mvp` 到 `argus@h1:/home2/argus/infra/mvp/src/mvp` - -### 3.2 执行 E2E - -在 h1: - -- [ ] `./scripts/run_all_v30_api.sh`(确保环境变量按脚本要求设置:`MVP_INTERNAL_TOKEN`、可选 `WANDB_API_KEY` 等) - -### 3.3 核心检查点 - -- [ ] PPO/GRPO/SFT 任务整体流程可执行(至少 PPO/GRPO 不因 rollout backend 初始化失败) -- [ ] 任一 PPO/GRPO 的 Ray job logs / submit payload / hydra overrides 中可确认: - - `actor_rollout_ref.rollout.name=vllm` - -**验收**: -- `run_all_v30_api.sh` 端到端成功(或若 PPO/GRPO 因 vllm 参数差异失败,需在本 milestone 内补齐必要 overrides 并重新跑通) - ---- - -## 风险与回滚策略 - -### 风险 - -- vLLM rollout 可能对部分参数(如 batch/并发/显存利用率)有不同约束,导致训练启动失败。 -- base image 切换导致 ray/依赖版本差异。 - -### 回滚 - -回滚到 v3.6 / sglang 的最小动作: -- `docker-compose.yaml` 恢复旧镜像 tag -- `builders.py` 恢复 rollout.name=sglang - diff --git a/specs/mvp/v3.7/v3.7_summary.md b/specs/mvp/v3.7/v3.7_summary.md deleted file mode 100644 index c00bf26..0000000 --- a/specs/mvp/v3.7/v3.7_summary.md +++ /dev/null @@ -1,121 +0,0 @@ -# MVP v3.7 迭代总结:切换 vLLM rollout + `verlai/verl:vllm011.latest` - -> 基线版本:v3.6(W&B + SFTPGo + WebUI/API + Ray stateless pool + Advanced TaskSpec) -> 验证环境:`argus@h1:/home2/argus/infra/mvp` - -## 1. 目标与结果 - -### 1.1 本次目标 - -1) Ray 节点镜像切换到 vLLM 版本: -- base image:`verlai/verl:vllm011.latest` -- 构建镜像 tag:`argus/argus-ray-node:vllm011.latest` - -2) 平台内置 PPO/GRPO 默认 rollout backend 全量切换: -- `actor_rollout_ref.rollout.name=sglang` → `actor_rollout_ref.rollout.name=vllm` - -3) 端到端验证: -- 使用 `src/mvp/scripts/run_all_v30_api.sh` 在 h1 上跑通 E2E(通过 API 提交 PPO/GRPO/SFT) - -### 1.2 实际结果(验收) - -- h1 上已成功构建并使用新镜像拉起(head + 2 worker): - - `docker ps` 显示 `argus-ray-head/worker-*` 使用 `argus/argus-ray-node:vllm011.latest` -- `run_all_v30_api.sh` 端到端跑通: - - PPO/GRPO/SFT 任务均 `SUCCEEDED` -- 在 job submit payload 中验证关键点: - - `actor_rollout_ref.rollout.name=vllm` - - `HF_HUB_OFFLINE=1`(见 §3.2) - ---- - -## 2. 代码与配置改动点 - -### 2.1 训练默认参数(sglang → vllm) - -- `src/mvp/py/argus/ray/builders.py` - - 将 PPO/GRPO 默认参数中的 `actor_rollout_ref.rollout.name` 固定为 `vllm` -- `src/mvp/py/argus/service/ui.py` - - New Task → Advanced example 同步改为 `actor_rollout_ref.rollout.name=vllm`(避免用户 copy/paste 走错) - -并用单测锁定行为(TDD): -- `src/mvp/py/tests/test_builders.py` -- `src/mvp/py/tests/test_ui.py` - -### 2.2 镜像与 compose(强制用预构建镜像) - -- `src/mvp/images/argus-ray-node/Dockerfile` - - 默认 `ARG BASE_IMAGE=verlai/verl:vllm011.latest` -- `src/mvp/docker-compose.yaml` - - 移除 `ray_head.build`(避免每次 `docker compose up` 触发 build) - - head/worker 统一使用 `image: argus/argus-ray-node:vllm011.latest` - ---- - -## 3. E2E 遇到的问题与修复 - -### 3.1 问题:vLLM 初始化触发 HF mirror 429 - -在切换到 vLLM rollout 后,PPO/GRPO 任务启动阶段出现: -- `huggingface_hub.errors.HfHubHTTPError: 429 Too Many Requests` -- 请求来源:`https://hf-mirror.com/api/models//tree/main?...` - -原因要点: -- 传入模型为 repo id(`Qwen/Qwen2.5-0.5B-Instruct`)时,vLLM 会调用 HF API 获取 repo tree/file list; -- 多进程/多 replica 并发会瞬间放大请求,导致 mirror 限流; -- 即便本地 cache 已存在,repo id 路径仍可能触发远端检查。 - -### 3.2 修复:禁用 HF Hub 联网 + 使用本地 snapshot path - -1) 在 Ray job runtime_env 注入离线开关: -- `src/mvp/configs/dev.yaml` -- `src/mvp/configs/dev_v30.yaml` - -新增: -```yaml -HF_HUB_OFFLINE: "1" -``` - -2) E2E 脚本提交任务时,`model_id` 改为本地 snapshot 目录,避免 repo id: -- `src/mvp/scripts/run_all_v30_api.sh` - - 在 head 容器内用 `snapshot_download(..., local_files_only=True)` 解析本地路径 - - 用该路径作为 `model_id:` 提交 PPO/GRPO/SFT - -> 结果:E2E 任务不再触发 HF mirror 429,PPO/GRPO/SFT 全部跑通。 - ---- - -## 4. 远端部署/操作记录(h1) - -### 4.1 构建镜像(h1 上执行) - -在 `argus@h1:/home2/argus/infra/mvp/src/mvp`: - -```bash -docker build -f images/argus-ray-node/Dockerfile \ - --build-arg BASE_IMAGE=verlai/verl:vllm011.latest \ - -t argus/argus-ray-node:vllm011.latest . -``` - -### 4.2 拉起环境(compose) - -```bash -docker compose down -docker compose up -d -``` - -### 4.3 E2E - -```bash -export MVP_INTERNAL_TOKEN=my-dev-token -export SFTPGO_ADMIN_PASSWORD=my-dev-sftpgo-admin -./scripts/run_all_v30_api.sh -``` - ---- - -## 5. 已知影响与注意事项 - -1) **vLLM rollout 更敏感于模型加载路径与联网行为**:建议默认离线(`HF_HUB_OFFLINE=1`)并优先使用本地 snapshot path。 -2) **镜像切换可能带来依赖差异**:后续若遇到 rollout 相关参数兼容问题,应以 vLLM 的配置要求为准逐项调整(保持小步快跑)。 - diff --git a/specs/mvp/v3.8/Snipaste_2026-01-08_17-17-57.png b/specs/mvp/v3.8/Snipaste_2026-01-08_17-17-57.png deleted file mode 100644 index 3ec8be7..0000000 Binary files a/specs/mvp/v3.8/Snipaste_2026-01-08_17-17-57.png and /dev/null differ diff --git a/specs/mvp/v3.8/ray_serve.md b/specs/mvp/v3.8/ray_serve.md deleted file mode 100644 index c2f8a96..0000000 --- a/specs/mvp/v3.8/ray_serve.md +++ /dev/null @@ -1,314 +0,0 @@ - -API参考资料 -https://docs.ray.io/en/latest/serve/api/doc/ray.serve.llm.LLMConfig.html - -ray.serve.llm.LLMConfig -pydantic model ray.serve.llm.LLMConfig[source] -The configuration for starting an LLM deployment. - -PublicAPI (alpha): This API is in alpha and may change before becoming stable. - -field accelerator_type: str | None = None -The type of accelerator runs the model on. Only the following values are supported: [‘V100’, ‘P100’, ‘T4’, ‘P4’, ‘K80’, ‘A10G’, ‘L4’, ‘L40S’, ‘A100’, ‘H100’, ‘H200’, ‘H20’, ‘B200’, ‘Intel-GPU-Max-1550’, ‘Intel-GPU-Max-1100’, ‘Intel-GAUDI’, ‘AMD-Instinct-MI100’, ‘AMD-Instinct-MI250X’, ‘AMD-Instinct-MI250X-MI250’, ‘AMD-Instinct-MI210’, ‘AMD-Instinct-MI300A’, ‘AMD-Instinct-MI300X-OAM’, ‘AMD-Instinct-MI300X-HF’, ‘AMD-Instinct-MI308X’, ‘AMD-Instinct-MI325X-OAM’, ‘AMD-Instinct-MI350X-OAM’, ‘AMD-Instinct-MI355X-OAM’, ‘AMD-Radeon-R9-200-HD-7900’, ‘AMD-Radeon-HD-7900’, ‘aws-neuron-core’, ‘TPU-V2’, ‘TPU-V3’, ‘TPU-V4’, ‘TPU-V5P’, ‘TPU-V5LITEPOD’, ‘TPU-V6E’, ‘Ascend910B’, ‘Ascend910B4’, ‘MXC500’, ‘MXC550’, ‘A100-40G’, ‘A100-80G’] - -field callback_config: CallbackConfig [Optional] -Callback configuration to use for model initialization. Can be a string path to a class or a Callback subclass. - -field deployment_config: Dict[str, Any] [Optional] -The Ray @server.deployment options. Supported fields are: name, num_replicas, ray_actor_options, max_ongoing_requests, autoscaling_config, max_queued_requests, user_config, health_check_period_s, health_check_timeout_s, graceful_shutdown_wait_loop_s, graceful_shutdown_timeout_s, logging_config, request_router_config. For more details, see the Ray Serve Documentation. - -field engine_kwargs: Dict[str, Any] = {} -Additional keyword arguments for the engine. In case of vLLM, this will include all the configuration knobs they provide out of the box, except for tensor-parallelism which is set automatically from Ray Serve configs. - -field experimental_configs: Dict[str, Any] [Optional] -Experimental configurations for Ray Serve LLM. This is a dictionary of key-value pairs. Current supported keys are: - stream_batching_interval_ms: Ray Serve LLM batches streaming requests together. This config decides how long to wait for the batch before processing the requests. Defaults to 50.0. - num_ingress_replicas: The number of replicas for the router. Ray Serve will take the max amount all the replicas. Default would be 2 router replicas per model replica. - -field llm_engine: str = 'vLLM' -The LLMEngine that should be used to run the model. Only the following values are supported: [‘vLLM’] - -field log_engine_metrics: bool | None = True -Enable additional engine metrics via Ray Prometheus port. - -field lora_config: Dict[str, Any] | LoraConfig | None = None -Settings for LoRA adapter. Validated against LoraConfig. - -field model_loading_config: Dict[str, Any] | ModelLoadingConfig [Required] -The settings for how to download and expose the model. Validated against ModelLoadingConfig. - -field placement_group_config: Dict[str, Any] | None = None -Ray placement group configuration for scheduling vLLM engine workers. Defines resource bundles and placement strategy for multi-node deployments. Should contain ‘bundles’ (list of resource dicts) and optionally ‘strategy’ (defaults to ‘PACK’). Example: {‘bundles’: [{‘GPU’: 1, ‘CPU’: 2}], ‘strategy’: ‘PACK’} - -field runtime_env: Dict[str, Any] | None = None -The runtime_env to use for the model deployment replica and the engine workers. - -apply_checkpoint_info(model_id_or_path: str, trust_remote_code: bool = False) → None[source] -Apply the checkpoint info to the model config. - -classmethod from_file(path: str, **kwargs) → ModelT -Load a model from a YAML file path. - -get_engine_config() → None | VLLMEngineConfig[source] -Returns the engine config for the given LLM config. - -LLMConfig not only has engine config but also deployment config, etc. - -get_or_create_callback() → CallbackBase | None[source] -Get or create the callback instance for this process. - -This ensures one callback instance per process (singleton pattern). The instance is cached so the same object is used across all hooks. - -Returns -: -Instance of class that implements Callback - -multiplex_config() → ServeMultiplexConfig[source] -classmethod parse_yaml(file, **kwargs) → ModelT -setup_engine_backend()[source] -update_engine_kwargs(**kwargs: Any) → None[source] -Update the engine_kwargs and the engine_config engine_kwargs. - -This is typically called during engine starts, when certain engine_kwargs (e.g., data_parallel_rank) become available. - -validator validate_accelerator_type » accelerator_type[source] -validator validate_deployment_config » deployment_config[source] -Validates the deployment config dictionary. - -validator validate_experimental_configs » experimental_configs[source] -Validates the experimental configs dictionary. - -validator validate_llm_engine » llm_engine[source] -Validates the llm_engine string value. - -validator validate_lora_config » lora_config[source] -Validates the lora config dictionary. - -validator validate_model_loading_config » model_loading_config[source] -Validates the model loading config dictionary. - -property input_modality: str -Returns the input modality of the model. There could be more types in the future. Right now assumes if the model doesn’t support version, it’ll be text. - -property max_request_context_length: int | None -property model_architecture: str -property model_id: str -property supports_vision: bool - -# Python API -ray serve api -https://docs.ray.io/en/latest/serve/api/index.html#serve-api - - -Python API -Writing Applications -serve.Deployment - -Class (or function) decorated with the @serve.deployment decorator. - -serve.Application - -One or more deployments bound with arguments that can be deployed together. - -Deployment Decorators -serve.deployment - -Decorator that converts a Python class to a Deployment. - -serve.ingress - -Wrap a deployment class with an ASGI application for HTTP request parsing. - -serve.batch - -Converts a function to asynchronously handle batches. - -serve.multiplexed - -Wrap a callable or method used to load multiplexed models in a replica. - -Deployment Handles -Note - -The deprecated RayServeHandle and RayServeSyncHandle APIs have been fully removed as of Ray 2.10. See the model composition guide for how to update code to use the DeploymentHandle API instead. - -serve.handle.DeploymentHandle - -A handle used to make requests to a deployment at runtime. - -serve.handle.DeploymentResponse - -A future-like object wrapping the result of a unary deployment handle call. - -serve.handle.DeploymentResponseGenerator - -A future-like object wrapping the result of a streaming deployment handle call. - -Running Applications -serve.start - -Start Serve on the cluster. - -serve.run - -Run an application and return a handle to its ingress deployment. - -serve.delete - -Delete an application by its name. - -serve.status - -Get the status of Serve on the cluster. - -serve.shutdown - -Completely shut down Serve on the cluster. - -serve.shutdown_async - -Completely shut down Serve on the cluster asynchronously. - -Configurations -serve.config.ProxyLocation - -Config for where to run proxies to receive ingress traffic to the cluster. - -serve.config.gRPCOptions - -gRPC options for the proxies. - -serve.config.HTTPOptions - -HTTP options for the proxies. - -serve.config.AutoscalingConfig - -Config for the Serve Autoscaler. - -serve.config.AutoscalingPolicy - -PublicAPI (alpha): This API is in alpha and may change before becoming stable. - -serve.config.AutoscalingContext - -Rich context provided to custom autoscaling policies. - -serve.config.AggregationFunction - -An enumeration. - -serve.config.RequestRouterConfig - -Config for the Serve request router. - -Schemas -serve.schema.ServeActorDetails - -Detailed info about a Ray Serve actor. - -serve.schema.ProxyDetails - -Detailed info about a Ray Serve ProxyActor. - -serve.schema.ApplicationStatusOverview - -Describes the status of an application and all its deployments. - -serve.schema.ServeStatus - -Describes the status of Serve. - -serve.schema.DeploymentStatusOverview - -Describes the status of a deployment. - -serve.schema.EncodingType - -Encoding type for the serve logs. - -serve.schema.AutoscalingMetricsHealth - -An enumeration. - -serve.schema.AutoscalingStatus - -An enumeration. - -serve.schema.ScalingDecision - -One autoscaling decision with minimal provenance. - -serve.schema.DeploymentAutoscalingDetail - -Deployment-level autoscaler observability. - -serve.schema.ReplicaRank - -Replica rank model. - -Request Router -serve.request_router.ReplicaID - -A unique identifier for a replica. - -serve.request_router.PendingRequest - -A request that is pending execution by a replica. - -serve.request_router.RunningReplica - -Contains info on a running replica. - -serve.request_router.FIFOMixin - -Mixin for FIFO routing. - -serve.request_router.LocalityMixin - -Mixin for locality routing. - -serve.request_router.MultiplexMixin - -Mixin for multiplex routing. - -serve.request_router.RequestRouter - -Abstract interface for a request router (how the router calls it). - -Advanced APIs -serve.get_replica_context - -Returns the deployment and replica tag from within a replica at runtime. - -serve.context.ReplicaContext - -Stores runtime context info for replicas. - -serve.get_multiplexed_model_id - -Get the multiplexed model ID for the current request. - -serve.get_app_handle - -Get a handle to the application's ingress deployment by name. - -serve.get_deployment_handle - -Get a handle to a deployment by name. - -serve.grpc_util.RayServegRPCContext - -Context manager to set and get gRPC context. - -serve.exceptions.BackPressureError - -Raised when max_queued_requests is exceeded on a DeploymentHandle. - -serve.exceptions.RayServeException - -serve.exceptions.RequestCancelledError - -Raise when a Serve request is cancelled. - -serve.exceptions.DeploymentUnavailableError - -Raised when a Serve deployment is unavailable to receive requests. \ No newline at end of file diff --git a/specs/mvp/v3.8/ray_serve_llm.md b/specs/mvp/v3.8/ray_serve_llm.md deleted file mode 100644 index 89e61aa..0000000 --- a/specs/mvp/v3.8/ray_serve_llm.md +++ /dev/null @@ -1,87 +0,0 @@ - -基于提供的来源,以下是使用 **Builder Pattern(构建器模式)** 结合 Ray Serve 和 vllm 动态部署**中型大语言模型(Medium-sized LLM)**的原理与操作方案。 - -### 一、 核心原理 - -1. **中型 LLM 定义**:中型模型(如 Llama-3.1-70B)通常具有约 70B 参数。它们通常运行在**单个节点**上,利用 **4 到 8 个 GPU**。 -2. **Builder Pattern 机制**:该模式通过 `build_openai_app` 函数提供高度抽象。开发者只需定义一个 `LLMConfig` 对象,即可自动构建并链接底层的 `LLMServer` 和 `OpenAiIngress` 组件。 -3. **高性能后端 (vLLM)**:Ray Serve LLM 使用 vLLM 作为推理引擎,支持高性能推理和显存管理。 -4. **动态扩缩容与资源调度**: - * **张量并行 (Tensor Parallelism)**:通过 `tensor_parallel_size` 将模型权重均匀分布在单节点的所有 GPU 上。 - * **副本缩放 (Autoscaling)**:通过 `autoscaling_config` 动态调整 `min_replicas` 和 `max_replicas`,使服务能根据实时流量增减推理副本。 - ---- - -### 二、 操作方案 - -#### 1. 环境准备 -确保已安装必要的依赖包并配置 Hugging Face 访问令牌(针对 Llama-3.1 等受限模型)。 -```bash -pip install "ray[serve,llm]" -export HF_TOKEN= -``` - -#### 2. 编写部署脚本 (`serve_medium_llm.py`) -使用 **Builder Pattern** 定义配置并构建应用。以下示例配置了一个典型的 70B 模型部署: - -```python -# serve_medium_llm.py -from ray.serve.llm import LLMConfig, build_openai_app -import os - -llm_config = LLMConfig( - model_loading_config=dict( - model_id="my-llama-3.1-70b", - model_source="meta-llama/Llama-3.1-70B-Instruct", - ), - accelerator_type="A100-40G", # 或 L40S - deployment_config=dict( - autoscaling_config=dict( - min_replicas=1, # 最小副本数 - max_replicas=4, # 最大副本数,实现动态扩展 - ) - ), - runtime_env=dict(env_vars={"HF_TOKEN": os.environ.get("HF_TOKEN")}), - engine_kwargs=dict( - max_model_len=32768, # 上下文长度 - tensor_parallel_size=8, # 在单节点的 8 个 GPU 间拆分权重 - ), -) - -# 使用 Builder Pattern 构建应用 -app = build_openai_app({"llm_configs": [llm_config]}) -``` - -#### 3. 启动部署 -在终端运行以下命令启动服务: -```bash -serve run serve_medium_llm:app -``` -部署过程通常需要几分钟,包括配置集群、启动 vLLM 服务器以及下载模型权重。 - -#### 4. 发送请求测试 -服务启动后,可以通过符合 OpenAI 标准的接口进行访问。 -```python -from openai import OpenAI - -client = OpenAI(base_url="http://localhost:8000/v1", api_key="FAKE_KEY") -response = client.chat.completions.create( - model="my-llama-3.1-70b", - messages=[{"role": "user", "content": "解释一下什么是量子纠缠?"}], - stream=True -) -for chunk in response: - if chunk.choices.delta.content: - print(chunk.choices.delta.content, end="", flush=True) -``` - ---- - -### 三、 性能与并发优化建议 - -* **提高并发量**:可以通过降低 `max_model_len` 来减少 KV 缓存所需的显存,从而显著提升每个副本支持的最大并发请求数。 -* **监控指标**:通过 Ray Serve LLM 仪表盘监控 **TTFT(首字延迟)**、**TPOT(单字延迟)** 和 **Token 吞吐量** 来评估服务性能。 -* **精度折衷**:对于资源受限的场景,可以使用**量化模型**(如 FP8)来减少模型内存占用,为 KV 缓存留出更多空间,进而提高并发能力。 - -**比喻理解**: -部署**中型 LLM** 就像是在一个大型车间里组装一台复杂的精密机器(模型权重)。**Builder Pattern** 是你的“全自动组装线”,你只需设定好机器的参数(Config),生产线就会自动帮你把零件固定好并接通电源。而 **vLLM 和张量并行** 就像是让 8 个熟练工人(GPU)共同抬起这台沉重的机器,每个人只负责自己那一部分的力气,从而让机器能够平稳地运转。 \ No newline at end of file diff --git a/specs/mvp/v3.8/requirements.md b/specs/mvp/v3.8/requirements.md deleted file mode 100644 index aaaa642..0000000 --- a/specs/mvp/v3.8/requirements.md +++ /dev/null @@ -1,8 +0,0 @@ - -1. 通过ray serve(后端vllm)来动态拉起llm,支持多模型application部署, -2. 默认一个模型只有一个replica,用户配置可以多个 -3. 用户可以删除(下线)模型 -4. 可以指定模型用几张卡 -5. 通过WebUI来进行配置,查看当前部署的模型列表,以及可以查看详情 -6. 模型路径可以使用common,也可以用户自己指定user路径 -7. \ No newline at end of file diff --git a/specs/mvp/v3.8/v3.8_api.md b/specs/mvp/v3.8/v3.8_api.md deleted file mode 100644 index 813f2a9..0000000 --- a/specs/mvp/v3.8/v3.8_api.md +++ /dev/null @@ -1,224 +0,0 @@ -# MVP v3.8 API Reference(Serving) - -> 说明:本节为 v3.8 新增的 **Model Serving** API(Ray Serve LLM / vLLM)。 -> 认证:Serving 管理 API 复用现有 MVP API 的认证方式(`Authorization: Bearer `)。 -> 推理:对外 OpenAI endpoint **不做鉴权**(v3.8 约定)。 - -## 0. 基本信息 - -### 0.1 Base URLs - -- MVP API server:`http://:8080` -- Ray Serve OpenAI ingress(固定端口 8000):`http://:8000/v1` - -### 0.2 认证 - -所有 `/api/v2/serve/*` 接口要求: - -``` -Authorization: Bearer -``` - -其中 `user_token` 由管理员通过 `/api/v2/users//tokens` 颁发(沿用现有机制)。 - -### 0.3 命名规则:`model_id = user_id-YYYYMMDDHHMM-` - -- 用户提交时填写 `model_id`(语义为 suffix,例如 `qwen-0.5b`) -- 平台生成前缀: - - `prefix = "-"` -- 平台实际对外暴露的 OpenAI model 名称为: - - `model_id = "-"` - - 示例:`alice-202601061235-qwen-0.5b` - -## 1. 数据结构 - -### 1.1 ServingSpec(YAML) - -请求体建议使用 YAML(与 TaskSpec 一致),示例: - -```yaml -model_id: qwen-0.5b # 必填:suffix(平台自动加 user_id- 前缀) -model_source: $HOME/common/hf/.../ # 必填:本地路径或 repo id;平台做 $HOME 宏替换与路径校验 -num_replicas: 1 # 可选,默认 1 -gpus_per_replica: 1 # 可选,默认 1 -# engine_kwargs: # 可选:vLLM 参数透传(白名单/黑名单由实现决定) -# max_model_len: 8192 -# gpu_memory_utilization: 0.9 -``` - -说明: -- `accelerator_type` 不在 ServingSpec 中暴露;由平台配置(`dev.yaml` 的 `serving.llm.accelerator_type`)统一注入到 Ray Serve LLM 的 `LLMConfig.accelerator_type`(dev/h1: `H20`)。 - -#### 宏替换 - -- `$HOME` → `/private/users/` -- `$HOME/common/hf` → `/private/hf` -- `$HOME/common/datasets` → `/private/datasets`(serving 不强依赖,但保留一致语义) - -#### 路径校验(v3.8 约定) - -`model_source` 允许: - -- `/private/hf/...`(common) -- `/private/users//...`(user) - -拒绝: - -- 其它用户目录 -- 非 `/private` 下路径 -- 空路径或包含 `..` 的可疑路径 - -### 1.2 ServingModel(响应体,JSON) - -```json -{ - "model_key": "svc-alice-20260106-123000-abcd", - "user_id": "alice", - "model_id": "alice-202601061235-qwen-0.5b", - "model_id_suffix": "qwen-0.5b", - "model_id_prefix": "alice-202601061235", - "model_source": "/private/hf/hub/models--.../snapshots/", - "num_replicas": 1, - "gpus_per_replica": 1, - "total_gpus": 1, - "state": "RUNNING", - "endpoint": { - "openai_base_url": "http://:8000/v1", - "model": "alice-202601061235-qwen-0.5b" - }, - "error_summary": null, - "created_at": "2026-01-06T12:30:00Z", - "updated_at": "2026-01-06T12:31:02Z" -} -``` - -## 2. 管理 API(MVP API server) - -### 2.1 Create / Upsert model - -`POST /api/v2/serve/models` - -#### Request - -- Header: `Content-Type: application/yaml` -- Body: ServingSpec(YAML) - -#### Response (202) - -```json -{ - "model_key": "svc-alice-20260106-123000-abcd", - "state": "QUEUED" -} -``` - -语义: -- 创建新模型(若 suffix 不存在) -- 或更新已有模型(若同一用户同一 suffix 已存在):更新 replicas/gpu 等配置,进入 `QUEUED` 等待 reconciler apply - -### 2.2 List models (current user) - -`GET /api/v2/serve/models` - -#### Response (200) - -```json -{ - "items": [ ... ServingModel ... ], - "openai_base_url": "http://:8000/v1" -} -``` - -### 2.3 Get model detail - -`GET /api/v2/serve/models/{model_key}` - -#### Response (200) - -```json -{ - "model": { ... ServingModel ... }, - "resolved_spec_yaml": "model_id: ...\nmodel_source: ...\n", - "events": [ - { "event_type": "DEPLOY_REQUESTED", "created_at": "...", "payload": {...} } - ], - "serve_status": { - "app_name": "argus_llm_app", - "app_status": "RUNNING" - } -} -``` - -### 2.4 Scale replicas (PATCH) - -`PATCH /api/v2/serve/models/{model_key}` - -#### Request (JSON) - -```json -{ "num_replicas": 2 } -``` - -#### Response (200) - -```json -{ "model_key": "...", "state": "QUEUED" } -``` - -> v3.8 只支持修改 `num_replicas`(以及可选 engine_kwargs);`gpus_per_replica` 若修改,可能触发重新部署。 - -### 2.5 Delete / Undeploy model - -`DELETE /api/v2/serve/models/{model_key}` - -#### Response (200) - -```json -{ "model_key": "...", "state": "DELETING" } -``` - -语义:从“声明式配置”中删除该模型,reconciler 会在下一轮 tick 触发 `serve.run(...)` 更新 app 配置并最终使其不可见。 - -### 2.6 Admin: Serve cluster status(可选) - -`GET /api/v2/serve/status` - -#### Response (200) - -返回 `serve.status()` 摘要(集群级 + app 级)。 - -> 仅 admin token 可访问(沿用 v3.x admin gate)。 - -## 3. 推理 API(Ray Serve OpenAI ingress) - -> v3.8 不做鉴权:无需 `Authorization`。 - -### 3.1 List models - -`GET http://:8000/v1/models` - -返回可用 model 列表(包含 `alice-qwen-0.5b` 这类带前缀名称)。 - -### 3.2 Chat completions - -`POST http://:8000/v1/chat/completions` - -```json -{ - "model": "alice-202601061235-qwen-0.5b", - "messages": [{"role":"user","content":"Hello"}], - "stream": false -} -``` - -### 3.3 Completions / Embeddings - -按 Ray Serve LLM OpenAI ingress 支持范围提供(v3.8 验收至少覆盖 chat)。 - -## 4. 错误码约定(MVP API server) - -- `400 invalid yaml/spec`:YAML 解析失败、字段缺失、值不合法 -- `403 forbidden`:路径越权(model_source 访问其他用户目录) -- `409 conflict`:model_id_suffix 冲突(同一用户重复创建且不允许覆盖时;若选择 upsert 则不返回该错误) -- `422 unprocessable`:资源参数非法(replica/gpu <=0) -- `500 internal`:reconciler/serve 调用异常(详情记录到 `serve_events`,并写入 `error_summary`) diff --git a/specs/mvp/v3.8/v3.8_design.md b/specs/mvp/v3.8/v3.8_design.md deleted file mode 100644 index a69a99c..0000000 --- a/specs/mvp/v3.8/v3.8_design.md +++ /dev/null @@ -1,371 +0,0 @@ -# MVP v3.8 详细设计方案:Ray Serve(vLLM)模型动态部署与管理 - -> 基线:当前已具备 v3.7 能力(训练平台 + W&B + SFTPGo + WebUI/API + Ray stateless pool,训练侧默认 rollout=vllm)。 -> v3.8 目标:在同一套 Ray 集群上,引入 **Ray Serve LLM(后端 vLLM)** 的模型推理服务能力,并通过 WebUI/API 动态管理模型生命周期。 - -## 0. 需求范围(来自 requirements.md) - -1) 通过 Ray Serve(后端 vLLM)动态拉起 LLM,支持**多模型 application** 部署 -2) 默认一个模型 1 个 replica,用户可配置多个 -3) 用户可删除(下线)模型 -4) 用户可指定模型使用几张 GPU -5) WebUI 可配置、查看模型列表、查看详情 -6) 模型路径可用 common,也可用 user 路径(本地路径) - -## 1. 总体架构 - -### 1.1 组件关系 - -v3.8 在现有“训练平台”之上新增一个 **Serving 子系统**: - -- **API server(现有)** - - 新增 Serving API(模型部署/删除/扩缩容/状态) - - 新增 Serving 后台线程(reconciler):周期性对齐 DB 与 Ray Serve 实际状态 -- **SQLite(现有)** - - 新增 `serve_models`、`serve_events` 等表,保存声明式配置与状态 -- **Ray 集群(现有 stateless pool)** - - 复用现有 head/worker 容器 - - 在集群内启动 Ray Serve(controller + proxy + deployments) -- **Ray Serve LLM(新增)** - - 通过 `ray.serve.llm.build_openai_app` 构建一个 OpenAI-compatible app - - app 内包含多个 `LLMConfig`(每个对应一个模型) - -### 1.2 为什么选择“单个 multi-model application” - -Ray Serve 支持 multi-app,但在 dev/docker 场景下多个 app 的 route_prefix 管理更复杂;同时 requirements 要求“多模型 application 部署”,因此 v3.8 采用: - -- 一个固定的 app:`argus_llm_app`(名字可配置) -- route_prefix 固定为 `/`(对外暴露 `/v1/...` OpenAI 接口) -- 每个模型对应一个 `LLMConfig`,通过 `model_id` 区分(即 OpenAI API 里的 `model` 字段) - -这样对用户而言最直观: - -- base_url 固定:`http://:8000/v1` -- `model=` 选择不同模型(`/v1/models` 自动列出) - -## 2. Ray Serve 部署策略(dev/h1 约束) - -### 2.1 HTTP 入口端口与 docker compose - -Ray Serve 默认 HTTP 端口是 `8000`。v3.8 约定: - -- 在 **head 容器** 映射 `8000:8000` -- API server 仍在 `8080` -- Ray Dashboard 在 `8265` - -原因:在单机多容器 docker 环境里,如果让 proxy “每个节点都起”,会出现多个容器同时想绑定同一个 host 端口的问题(不可行)。因此 v3.8 推荐: - -- Serve proxy 位置设为 **HeadOnly**(只在 head 上提供 HTTP 入口) -- GPU replica 仍运行在 worker 上(proxy 只转发,不跑推理) - -> 需要注意: -> - Serve 的 HTTP 配置(host/port/proxy_location)是 **Ray 集群全局配置**,启动后无法动态修改,因此应当在平台启动时一次性设定并持久化。 -> - proxy Actor 需要 CPU 资源;head 节点的 `num-cpus=0` 策略可能需要在 v3.8 做小幅调整(例如给 head 保留少量 CPU),但仍通过 `entrypoint_resources` 确保训练 driver 不会被调度到 head。 - -#### 2.1.1 compose 预期改动(v3.8 实现时落地) - -- `src/mvp/docker-compose.yaml`(ray_head)新增: - - `ports: - "8000:8000"` - -> worker 容器不暴露 8000(避免 host 端口冲突),由 head proxy 统一对外提供入口。 - -### 2.2 启动/配置方式(Python SDK 优先) - -v3.8 采用 Ray Serve Python SDK: - -- `ray.init(address="auto")` -- `serve.start(proxy_location="HeadOnly", http_options={"host":"0.0.0.0","port":8000})`(一次性全局配置) -- `serve.run(app, name=, route_prefix="/")` -- `serve.delete(name=)`(必要时) -- `serve.status()` 查询集群/应用状态 - -理由: - -- 避免在平台内部引入额外 REST client 依赖(并减少跨版本 REST schema 不稳定风险) -- API server 本身运行在 head 容器内,可直接 `ray.init(address="auto")` 连接现有集群 - -> 另:Ray Dashboard 暴露 Serve REST API(`PUT /api/serve/applications/` 等)可作为备选方案,但 v3.8 先不以它为主通路。 - -### 2.3 依赖与镜像假设 - -v3.8 依赖: - -- `ray[serve]`(Serve Controller/Proxy) -- `ray[llm]`(Ray Serve LLM 的 `ray.serve.llm` 模块) -- vLLM(推理引擎) - -由于 v3.7 已切换到 `verlai/verl:vllm011.latest`,预期镜像内包含 vLLM;但 `ray.serve.llm` 是否开箱即用需要在实现阶段确认。 -若缺失,v3.8 将在 `argus-ray-node` 镜像构建阶段补充 `pip install "ray[serve,llm]"`(或按官方建议的最小依赖)并做版本锁定。 - -### 2.4 Serving 配置(dev.yaml) - -v3.8 新增一段 serving 配置,至少包含: - -```yaml -serving: - serve: - http_port: 8000 # 固定 8000 - proxy_location: HeadOnly # dev/docker 下推荐 - llm: - accelerator_type: H20 # dev 环境填写 H20(对应 ray.serve.llm.LLMConfig.accelerator_type) -``` - -说明: -- `accelerator_type` 是 Ray Serve LLM 的 `LLMConfig.accelerator_type` 字段,用于表达“该模型运行在哪类加速卡上”。在 dev/h1 环境我们固定为 `H20`。 -- v3.8 不把 `accelerator_type` 暴露给普通用户编辑(避免误配);由部署环境配置统一决定。 - -## 3. 模型配置与资源映射 - -### 3.1 关键配置对象:`ray.serve.llm.LLMConfig` - -每个模型部署由一个 `LLMConfig` 描述,关键字段(v3.8 用到的子集): - -- `model_loading_config` - - `model_id`: 对外展示/请求时用的模型名(唯一 key) - - `model_source`: HF repo id / S3 / **local path** -- `accelerator_type` - - 从 `dev.yaml` 的 `serving.llm.accelerator_type` 读取(dev/h1: `H20`) -- `deployment_config` - - `num_replicas` 或 `autoscaling_config`(v3.8 先用固定 `num_replicas`) - - `ray_actor_options`(CPU/资源约束) -- `engine_kwargs` - - vLLM 相关参数(`max_model_len`、`gpu_memory_utilization` 等) -- `placement_group_config` - - 控制 vLLM engine workers 使用的资源 bundle(用于多 GPU / 跨节点) -- `runtime_env` - - 注入 HF cache、离线开关等环境变量 - -### 3.2 GPU 张数(gpus_per_replica)如何落到 LLMConfig - -v3.8 把用户输入的: - -- `gpus_per_replica = N` - -映射为: - -- `engine_kwargs.tensor_parallel_size = N`(单机/跨机张量并行,Ray Serve LLM 官方示例写法) -- `placement_group_config = {"bundles": [{"GPU": 1, "CPU": }] * N, "strategy": "PACK"}` - -并在 `engine_kwargs` 中保留 vLLM 其他参数(`max_model_len`、`gpu_memory_utilization` 等)。 - -> 兼容性说明:Ray Serve LLM/Serve LLM 仍处于快速演进阶段;v3.8 会以我们线上实际 Ray 版本为准做最小适配与回归测试。 - -### 3.2.1 跨节点场景(N > 单机 GPU) - -Ray Serve LLM 默认使用 `PACK` 策略,优先把 GPU worker 放在尽量少的节点上;如果单机放不下,会自动 spill 到其它节点,从而支持跨节点张量并行(TP)部署。 - -### 3.3 replica 数(num_replicas) - -v3.8 默认: - -- `num_replicas = 1` - -允许用户在 UI 中设置为 `>=1`。 -多 replica 会线性消耗 GPU(`num_replicas * gpus_per_replica`),需要做资源预检查。 - -### 3.4 模型路径与宏替换(common / user) - -v3.8 支持两类模型来源: - -1) **common** -- 典型为 `/private/hf/...`(共享 HF cache / snapshot) - -2) **user** -- `/private/users//models/...` -- 以及用户训练输出(例如 `jobs//checkpoints/.../huggingface`) - -为保证 UI 易用,沿用平台已有的宏语义: - -- `$HOME` → `/private/users/` -- `$HOME/common/hf` → `/private/hf` - -并进行路径校验: - -- 允许前缀:`/private/hf`、`/private/users//` -- 拒绝:越权访问其他用户目录、或访问系统敏感路径 - -### 3.5 离线模式(避免 HF mirror 429) - -v3.7 训练侧已验证 `HF_HUB_OFFLINE=1` 的必要性。v3.8 Serving 侧同样默认注入: - -- `HF_HOME=/private/hf` -- `HUGGINGFACE_HUB_CACHE=/private/hf/hub` -- `TRANSFORMERS_CACHE=/private/hf/transformers` -- `HF_HUB_OFFLINE=1` -- `HF_ENDPOINT=https://hf-mirror.com`(可保留,但离线模式下不应触发网络) - -并建议用户在 ServingSpec 中尽量填写 **local path** 作为 `model_source`,而不是直接 repo id。 - -## 4. 平台数据模型(SQLite) - -新增两张主表: - -### 4.1 `serve_models` - -每一行代表一个“声明式模型部署”: - -- `model_key`(平台内部唯一 ID,便于重命名/去重) -- `user_id` -- `model_id`(对外 OpenAI model 名称,要求 per-app 唯一) -- `model_source`(本地路径或 repo id,存 resolved 后的结果) -- `num_replicas` -- `gpus_per_replica` -- `engine_kwargs_json`(可选) -- `state`:`QUEUED | DEPLOYING | RUNNING | FAILED | DELETING | DELETED` -- `serve_app_name`(默认 `argus_llm_app`) -- `created_at / updated_at` -- `error_summary` - -### 4.2 `serve_events` - -记录关键事件与排障信息(类似 task_events): - -- `id` -- `model_key` -- `event_type`(DEPLOY_REQUESTED/DEPLOY_APPLIED/STATUS_SYNC/DELETE_REQUESTED/...) -- `payload_json` -- `created_at` - -## 5. API 设计(新增) - -在现有 `Authorization: Bearer ` 的认证体系下,新增 Serving API(路径仅示意,具体在实现时与现有 `api/v2` 对齐)。 - -### 5.1 用户接口 - -- `POST /api/v2/serve/models` - - body: YAML 或 JSON(v3.8 先用 YAML 与现有 TaskSpec 一致) - - 创建/更新(upsert)一个模型配置,进入 `QUEUED` -- `GET /api/v2/serve/models` - - 列出当前用户的模型列表(含 state、资源、endpoint) -- `GET /api/v2/serve/models/{model_key}` - - 详情:完整 spec + 最近事件 + Serve status 摘要 -- `PATCH /api/v2/serve/models/{model_key}` - - 修改 `num_replicas`、或 engine_kwargs(可选) -- `DELETE /api/v2/serve/models/{model_key}` - - 下线模型(进入 `DELETING`) - -### 5.2 系统接口(admin) - -- `GET /api/v2/serve/status`(admin) - - 返回 `serve.status()` 的摘要(集群级 / app 级) - -### 5.3 对外推理 endpoint - -固定输出到 UI/接口中: - -- `openai_base_url = http://:8000/v1` -- 支持: - - `/v1/chat/completions` - - `/v1/completions` - - `/v1/embeddings` - - `/v1/models` - -> v3.8 不做额外网关与鉴权(保持与现有 dev 环境一致);若后续需要,可在 v3.9+ 引入 token 校验/反向代理。 - -### 5.4 `model_id` 前缀策略(user_id-) - -为避免多用户冲突并保持可读性: - -v3.8 采用“**user_id + 日期小时分钟**”作为稳定前缀,以降低冲突并便于快速定位创建时间: - -- 用户在 UI/API 中仅填写 `model_id_suffix`(或仍用字段名 `model_id`,但语义为 suffix) -- 平台计算实际对外 `model_id`: - - `prefix = f"{user_id}-{YYYYMMDDHHMM}"` - - `model_id = f"{prefix}-{model_id_suffix}"` -- 在列表/详情中同时展示: - - `model_id_suffix`(用户输入) - - `model_id_prefix`(平台生成,例如 `alice-202601061235`) - - `model_id`(对外 OpenAI 名称) - -## 6. 后台执行模型(Serving Reconciler) - -v3.8 参考任务 scheduler 的模式,引入一个轻量的 reconciler: - -- tick 周期(例如 5s) -- 每次 tick: - 1) 拉取 DB 中 `QUEUED/DEPLOYING/RUNNING/DELETING` 的模型 - 2) 调用 `serve.status()` 读取当前 app 及 deployments 状态 - 3) 若存在 `QUEUED` 或需要变更的模型:构建新的 multi-model app(包含全部 `RUNNING/DEPLOYING/QUEUED` 的模型配置)并 `serve.run(...)` - 4) 若存在 `DELETING`:从 app 配置中移除对应模型,并 `serve.run(...)` 应用变更 - 5) 更新每个模型的 state(依据 Serve status) - -重要行为说明(multi-model app 的代价): -- 每次“新增/删除/改 replicas”都会触发对同一个 app 的一次 `serve.run(...)` 更新; -- Ray Serve 会尽量做增量更新,但在某些版本/配置下可能导致 ingress/router 短暂重启; -- v3.8 先接受该代价(满足需求闭环优先);若后续需要“删除某模型不影响其它模型”,可演进为“每模型一个 app + 单独 route_prefix”的方案。 - -资源预检查: -- 在 apply 前使用 `ray.available_resources()` 做粗粒度 GPU 预检查: - - 需要 GPU 总量 = `sum(num_replicas * gpus_per_replica)`(仅对“新增/扩容的差量”更精确) -- 若不足: - - 模型保持 `QUEUED`,记录事件 `PENDING_RESOURCES` - - 用户 UI 显示“资源不足,等待释放” - -> v3.8 不引入更复杂的抢占/优先级。Serving 与 Training 会竞争 GPU;用户需要自行规划资源(或后续版本引入统一调度)。 - -## 7. WebUI 设计(新增 Serving 页面) - -新增侧边栏入口:**Serving** - -### 7.1 Serving 列表页 - -- 展示字段: - - model_id - - user_id(仅 admin 可见) - - replicas / gpus_per_replica / total_gpus - - state(RUNNING/DEPLOYING/QUEUED/FAILED) - - 操作:Scale(修改 replicas)、Delete - -### 7.2 Serving 创建/编辑页 - -两种模式(与 New Task 类似,先做 YAML 模式即可): - -示例 YAML(v3.8): - -```yaml -model_id: qwen-0.5b -model_source: $HOME/common/hf/hub/models--Qwen--Qwen2.5-0.5B-Instruct/snapshots/ -num_replicas: 1 -gpus_per_replica: 1 -# engine_kwargs: -# max_model_len: 8192 -# gpu_memory_utilization: 0.9 -``` - -### 7.3 Serving 详情页 - -- 完整配置(resolved spec) -- Serve status 摘要(deployments 状态、replica 健康) -- OpenAI 调用示例(python openai client) - -## 8. 验收标准(v3.8) - -1) 部署: -- 一键部署一个模型(1 replica、1 GPU)成功,状态变为 RUNNING -- `/v1/models` 可列出该模型 - -2) 扩缩容: -- 修改 `num_replicas` 生效(Serve status 看到副本数变化) - -3) 多模型: -- 同一个 app 内能同时部署 2 个模型(不同 model_id) -- 通过 OpenAI 接口用不同 `model=` 请求可得到响应 - -4) 下线: -- 删除某模型后 `/v1/models` 不再出现 - -5) 模型路径: -- 支持 `/private/hf/...`(common)与 `/private/users//...`(user)两类本地路径 - -6) 资源不足可解释: -- 当 GPU 不足时,模型进入 `QUEUED` 并在 UI/详情中提示“资源不足” - -## 9. 待确认点(请你评审时确认) - -已确认(来自评审): - -1) 推理端口固定使用 `8000`(Ray Serve 默认端口)。 -2) 对外暴露的 OpenAI 接口 **不与现有 token 体系绑定**(v3.8 不做推理侧鉴权)。 -3) `model_id` 命名规则:平台统一加 `user_id + 日期小时分钟` 前缀,用户在 UI 里只填写后缀部分。 - -> 说明:这样可以避免跨用户 model_id 冲突,同时在 OpenAI API 的 `model=` 字段上自然可读。 diff --git a/specs/mvp/v3.8/v3.8_dev_plan.md b/specs/mvp/v3.8/v3.8_dev_plan.md deleted file mode 100644 index 49950f9..0000000 --- a/specs/mvp/v3.8/v3.8_dev_plan.md +++ /dev/null @@ -1,266 +0,0 @@ -# MVP v3.8 开发计划(TDD,细化版) - -> 目标:在 v3.7 基础上引入 Ray Serve(vLLM)模型动态部署与管理(多模型单 app),并提供 WebUI + API 管理闭环。 -> 约束(已确认): -> - 推理端口固定 `8000`(Serve HTTP)。 -> - 推理侧不接入现有 token 鉴权(对外 OpenAI endpoint 无鉴权)。 -> - 对外 `model_id` 统一加前缀:`--`(用户只填 suffix)。 -> - `LLMConfig.accelerator_type` 从 `dev.yaml` 读取(dev/h1: `H20`)。 - -本计划按“测试先行 → 实现 → 回归”的节奏拆分到可验证粒度;每个 milestone 都能单独验收。 - ---- - -## M0 - 基线与依赖探测(不改行为) - -**目的**:确认 v3.7 baseline 稳定,并明确 Ray Serve LLM 依赖是否已具备(否则后续会卡在镜像/依赖)。 - -### M0.1 本地回归 -- [ ] `.venv/bin/python -m pytest` 通过(coverage ≥ 90%) - -### M0.2 远端回归(h1) -- [ ] `src/mvp/scripts/run_all_v30_api.sh` 可跑通(确认训练闭环未回退) - -### M0.3 head 容器内依赖探测(记录结论) -- [ ] `python3 -c "import ray; import ray.serve; print(ray.__version__)"` -- [ ] `python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"` -- [ ] 若失败(例如缺 `gymnasium`):记录缺失项,并在 M6 通过补齐 `ray[llm]` 解决 - -### M0.4 配置探测 -- [ ] `configs/dev.yaml` 中存在: - - `serving.llm.accelerator_type: H20` - - `serving.serve.http_port: 8000` - - `serving.serve.proxy_location: HeadOnly` - -**验收**: -- baseline 无回退;依赖探测结论明确(可用/不可用) - ---- - -## M1 - ServingSpec(解析/校验/宏替换/路径校验)(单测驱动) - -**目的**:先把“输入”这层彻底固化(API/UI 复用),避免后期反复改 schema。 - -### M1.1 新增/扩展数据模型 -- [ ] `ServingSpec`(输入) - - `model_id`(suffix) - - `model_source`(支持 `$HOME` 宏) - - `num_replicas`(default=1) - - `gpus_per_replica`(default=1) - - `engine_kwargs`(可选 dict,先原样存 DB;实现阶段再做白名单/黑名单) -- [ ] `ResolvedServingSpec`(内部) - - `model_id_suffix` - - `model_id_prefix`(由平台生成:`user_id-YYYYMMDDHHMM`) - - `model_id`(对外:`-`) - - `model_source`(resolved path) - -### M1.2 规则(写成纯函数,便于测) -- [ ] `validate_model_id_suffix(suffix)`:长度/字符集限制(建议:`[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}`) -- [ ] `$HOME` 宏替换:`$HOME`、`$HOME/common/hf`、`$HOME/common/datasets` -- [ ] 路径校验(强制本地路径): - - 允许:`/private/hf/...`、`/private/users//...` - - 拒绝:`..`、空、其它用户路径、非 `/private` 路径 -- [ ] `make_model_id_prefix(user_id, now_utc)`:`YYYYMMDDHHMM`(UTC)+ user_id - -### M1.3 单测(先写失败用例,再补实现) -- [ ] `test_serving_spec_validation.py` - - suffix 合法/非法 - - replicas/gpus 边界:0、负数、小数、超大值(按实现决定是否限制上限) -- [ ] `test_serving_spec_paths.py` - - `$HOME` 替换正确 - - 越权路径返回 403/ValueError(按接口层映射) - - `/private/hf` 与 `/private/users/` 均可 -- [ ] `test_serving_model_id_prefix.py` - - 固定时间输入 → prefix 输出一致(避免时区/格式问题) - -**验收**: -- 输入 spec 规则稳定;核心校验/替换均有单测覆盖 - ---- - -## M2 - SQLite 表结构与 Db 接口(单测驱动) - -**目的**:Serving 的声明式状态必须持久化,可审计、可恢复。 - -### M2.1 DB schema -- [ ] `serve_models` - - 主键:`model_key`(平台生成) - - unique:`(user_id, model_id_suffix)`(实现 upsert) - - 存储:resolved spec(包含 prefix/full model_id、resolved model_source) - - 状态:`QUEUED/DEPLOYING/RUNNING/FAILED/DELETING/DELETED` - - `error_summary` -- [ ] `serve_events`(append-only) - -### M2.2 Db 方法 -- [ ] `upsert_serve_model(user_id, spec_yaml, now)` → (model_key, state) -- [ ] `list_serve_models(user_id, include_deleted=False, limit/offset?)` -- [ ] `get_serve_model(model_key)` -- [ ] `set_serve_model_state(model_key, state, error_summary=None)` -- [ ] `append_serve_event(model_key, event_type, payload_json=None)` -- [ ] `pick_next_runnable_serve_change()`(给 reconciler 用) - -### M2.3 单测 -- [ ] `test_db_serving.py` - - upsert 行为(同 suffix 更新不产生新 model_key 或产生新版本——此处需在实现前明确策略) - - state 流转 + 事件记录 - - list 的过滤与排序(按 updated_at) - -**验收**: -- DB 行为可预测;upsert/unique 语义确定并测试覆盖 - ---- - -## M3 - Serving 管理 API(FastAPI)(单测驱动) - -**目的**:先把管理 API 跑通,Ray Serve 先不接真实(reconciler 之后再接)。 - -### M3.1 API 路由(用户) -- [ ] `POST /api/v2/serve/models`(Content-Type: application/yaml) - - 入参:ServingSpec YAML - - 出参:`{model_key,state}`(202) -- [ ] `GET /api/v2/serve/models` - - 返回 items + `openai_base_url=http://:8000/v1` -- [ ] `GET /api/v2/serve/models/{model_key}` - - 返回 model + resolved_spec_yaml + events(分页可后置)+ serve_status(先空/占位) -- [ ] `PATCH /api/v2/serve/models/{model_key}`(JSON) - - 支持 `num_replicas`(最小闭环) -- [ ] `DELETE /api/v2/serve/models/{model_key}` - -### M3.2 API 路由(admin,可选) -- [ ] `GET /api/v2/serve/status`(仅 admin token) - -### M3.3 错误映射(必须测试) -- [ ] YAML 解析失败:400 -- [ ] spec 校验失败:422 -- [ ] 越权路径:403 -- [ ] 不存在 model_key:404 - -### M3.4 单测 -- [ ] `test_app_serving_api.py` - - happy path:create → list → get → patch → delete - - 多用户隔离:用户只能看到自己的 model - - 错误码覆盖:400/403/404/422 - -**验收**: -- API reference (`v3.8_api.md`) 中所有管理接口可返回预期结构(Serve 未接入也能工作) - ---- - -## M4 - ServeClient 抽象 + LLMConfig builder(单测驱动) - -**目的**:将“如何从 ResolvedServingSpec 构造 LLMConfig”固化,并把 Ray Serve 的依赖隔离到 client 里,便于 mock。 - -### M4.1 `ServeClient` 接口(可 mock) -- [ ] `ensure_started(http_port=8000, proxy_location="HeadOnly")` -- [ ] `apply_app(app_name, llm_configs)`(multi-model) -- [ ] `get_status()`(serve.status 摘要) - -### M4.2 `build_llm_config(resolved_spec, accelerator_type, runtime_env_defaults)` 纯函数 -- [ ] 写入 `LLMConfig.accelerator_type`(来自 dev.yaml:H20) -- [ ] `deployment_config.num_replicas` -- [ ] `engine_kwargs.tensor_parallel_size = gpus_per_replica` -- [ ] `placement_group_config` bundles 按 GPU 张数生成 -- [ ] `runtime_env.env_vars` 注入(至少包含 HF cache + `HF_HUB_OFFLINE=1`) - -### M4.3 单测 -- [ ] `test_llm_config_builder.py` - - gpus_per_replica=1/2/4 → tensor_parallel_size 与 bundles 数量正确 - - accelerator_type 注入正确 - - runtime_env 含 HF_HUB_OFFLINE 等关键 env - -**验收**: -- 从平台 spec 到 Ray Serve LLMConfig 的映射规则稳定,有单测锁定 - ---- - -## M5 - Serving Reconciler(状态机 + 资源预检查)(单测驱动) - -**目的**:实现声明式对齐:DB → Serve;同时提供可解释的 QUEUED/FAILED 状态。 - -### M5.1 状态机(最小闭环) -- [ ] `QUEUED`:等待 apply -- [ ] `DEPLOYING`:已触发 apply,等待 Serve running/healthy -- [ ] `RUNNING`:Serve status running -- [ ] `FAILED`:apply 或 status 失败(写 error_summary + event) -- [ ] `DELETING`:等待从 app 中移除 -- [ ] `DELETED`:完成删除(可选保留记录) - -### M5.2 资源预检查 -- [ ] `needed_total_gpus = sum(num_replicas*gpus_per_replica)`(最小可用预检查) -- [ ] `ray.available_resources()["GPU"]`(或更稳健的 per-node 统计)不足时: - - 保持 `QUEUED` - - 记录 `PENDING_RESOURCES` event - -### M5.3 reconcile 策略(multi-model app) -- [ ] tick 读取 active models,构建全量 `llm_configs` -- [ ] 处理 deleting:从 configs 中移除对应 model,再 apply - -### M5.4 单测(mock ServeClient + mock ray resources) -- [ ] `test_serving_reconciler.py` - - 新增模型:apply_app 被调用;state 进入 DEPLOYING - - 删除模型:apply_app configs 不包含该模型 - - GPU 不足:不 apply;state 仍 QUEUED;event 写入 - - apply 抛异常:state FAILED;error_summary 写入 - -**验收**: -- reconciler 行为在纯单测环境可验证;失败可解释 - ---- - -## M6 - 真实集成(h1):Ray Serve 启动 + 推理闭环(E2E) - -**目的**:在 dev/h1 环境真正跑通:部署模型 → `/v1/models` 可见 → `chat/completions` 成功 → 删除后消失。 - -### M6.1 compose/端口 -- [ ] `src/mvp/docker-compose.yaml`:`ray_head` 增加 `8000:8000` - -### M6.2 镜像依赖(若 M0 发现缺失) -- [ ] 在 `argus-ray-node` 镜像中补齐 `ray[serve,llm]`(版本与现有 Ray 对齐,避免升级 Ray 导致不兼容) - - 推荐优先补齐 `ray[llm]`(包含 `ray.serve.llm` 依赖闭包,如 `gymnasium`),再按需补 `ray[serve]` - - 验证点:`python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"` - -### M6.3 E2E 脚本(幂等) -- [ ] 新增 `scripts/run_all_v38_serving.sh`: - - 起 compose(确保 Serve 端口可用) - - 起 API - - 创建 user + token - - `POST /api/v2/serve/models` 创建 1GPU 模型 - - 轮询模型 state 到 RUNNING - - `curl http://127.0.0.1:8000/v1/models` 验证包含 `-` - - `curl http://127.0.0.1:8000/v1/chat/completions` 进行最小推理 - - `DELETE /api/v2/serve/models/{model_key}` 下线 - - 再轮询 `/v1/models` 不包含 - -**验收**: -- E2E 可重复跑通(至少两次连续跑不需要人工清理) - ---- - -## M7 - WebUI(Serving 页面)(单测驱动) - -**目的**:给用户可视化的模型管理页面(最小必要功能)。 - -### M7.1 页面 -- [ ] Sidebar 增加 Serving -- [ ] `/ui/serving`:列表 + 状态 + 操作(delete/scale) -- [ ] `/ui/serving/new`:YAML 输入 + submit -- [ ] `/ui/serving/{model_key}`:详情(resolved spec、events、OpenAI 调用示例) - -### M7.2 单测 -- [ ] `test_ui_serving.py`:路由 200、关键链接存在、包含 openai_base_url=8000 - -**验收**: -- WebUI 覆盖 create/list/detail/scale/delete 的主链路 - ---- - -## M8 - 文档与验收用例(交付) - -**目的**:给用户/运维一套可复用的运行方式与排障路径。 - -- [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`(按 milestone 记录) -- [ ] 补充 README(可选):端口说明、推理 API 无鉴权警示、模型路径约定 -- [ ] 验收清单(checklist): - - 单测通过 - - h1 E2E 通过 - - UI 主链路可操作 diff --git a/specs/mvp/v3.8/v3.8_per_model_app.md b/specs/mvp/v3.8/v3.8_per_model_app.md deleted file mode 100644 index fec725f..0000000 --- a/specs/mvp/v3.8/v3.8_per_model_app.md +++ /dev/null @@ -1,189 +0,0 @@ -# v3.8 方案补充:每个模型一个 Ray Serve App(隔离增删影响) - -## 背景与问题复现 - -当前 v3.8 的实现采用 **单 application + 多模型** 的方式: - -- 服务层每次 reconcile 都会构造“全量 llm_configs”并调用一次 `serve.run(app, name="argus_llm_app", route_prefix="/")` -- **新增/删除一个模型**会触发对同一个 app 的“整体更新” -- Ray Serve 在 app 更新时会对该 app 内的 deployments/replicas 做滚动更新与重新调度 - -因此你在 Ray Dashboard 中观察到: - -- 添加/删除一个模型时,其他模型的 Serve deployment 也进入更新状态 -- 内存/显存占用重新变化,甚至出现 GPU 卡位变化(replica 重新调度到不同 node/GPU) - -这与“其他未变更 model 不受影响”的期望不一致。 - ---- - -## 目标 - -将 serving 架构调整为: - -- **每个模型一个 Serve App(独立 app name)** -- 每个模型一个独立 `route_prefix` -- 新增/删除/缩放某个模型只更新该模型对应的 app,不影响其他模型 app - -约束保持不变: - -- 推理端口固定 `8000` -- 推理侧不接入现有 token 鉴权(OpenAI endpoint 无鉴权) -- `model_id` 前缀规则:`--` -- `LLMConfig.accelerator_type` 由 `configs/dev.yaml` 配置(dev/h1: `H20`) - ---- - -## 总体设计 - -### 1) 命名与路由 - -为每个 model 生成: - -- `app_name`:建议直接使用 `model_key`(天然唯一且 URL-safe),例如: - - `app_name = "mvp2-alice-serve-20260106-060203-aad8"` -- `route_prefix`:建议使用 model_key,避免 model_id 中的 `.`、`_` 等带来的 URL/编码歧义: - - `route_prefix = f"/serve/{model_key}"` - -于是该模型的 OpenAI base url 为: - -- `openai_base_url = http://:8000/serve//v1` - -说明: - -- 仍然是 **OpenAI-compatible**,只是 base_url 不再是根路径 `/v1`,而是每个模型一个前缀。 -- 这样可以做到“每个模型的 OpenAI endpoint 互不影响”,也更容易做按模型的观测/下线。 - -### 2) 运行方式(Ray Serve) - -单模型 app 的创建/更新: - -- `app = build_openai_app({"llm_configs":[LLMConfig(...)]})` -- `serve.run(app, name=app_name, route_prefix=route_prefix)` - -单模型 app 的删除: - -- `serve.delete(app_name)` - -关键点: - -- **更新/删除只作用于对应 app_name**;其它 app 不会被 serve.run “整体重建”触发滚动更新。 - -### 3) 服务层(Scheduler/Reconciler)改造点(高层) - -现状:`ServingReconciler.tick()` 每次对“全量模型集合” apply 一次 app。 - -目标:改成按 model_key 的“局部 reconcile”: - -- 对于状态 `QUEUED` 的 model: - - 只构建该 model 的 `LLMConfig` - - `serve.run(app, name=model_key, route_prefix="/serve/")` - - 状态:`DEPLOYING` →(probe 成功)→ `RUNNING` -- 对于状态 `DELETING` 的 model: - - `serve.delete(model_key)` - - 状态:`DELETED` - -资源预检查: - -- 只需要预检查“本次变更模型”需要的 GPU(`num_replicas * gpus_per_replica`) -- 不需要把其他模型资源都算入 needed_total_gpus(因为不再重建全量 app) - -### 4) API/UI 返回的 endpoint 结构 - -现状 API 返回: - -- `endpoint.openai_base_url = http://:8000/v1` -- `endpoint.model = ` - -建议改为(字段不变,值变化): - -- `endpoint.openai_base_url = http://:8000/serve//v1` -- `endpoint.model = `(保持) - -UI 的示例 curl 也应使用上面的 base_url。 - ---- - -## 行为变化与兼容性影响 - -### 1) `/v1/models` 聚合能力变化(重要) - -采用“每模型一个 route_prefix”后: - -- `http://:8000/v1/models` **不再是“所有模型的总览”**(除非我们再提供一个聚合层) -- 每个模型的 models list 在它自己的前缀下: - - `http://:8000/serve//v1/models` - -如果仍然希望保留一个统一入口(可选增强,非本方案必做): - -- 额外引入一个“稳定不重建”的 **OpenAI Router**(可以是: - - FastAPI(8080) 侧做反向代理;或 - - 一个单独 Ray Serve app 只负责路由,不随模型变更重建) -- Router 读取 SQLite/内存缓存的 model 映射: - - `model_id -> route_prefix` -- 将 `/v1/chat/completions` 转发到对应 model 的 prefix - -这可以作为 v3.9+ 的增强项;v3.8 的核心目标是“变更隔离”,优先保证稳定性。 - -### 2) 资源与调度稳定性 - -改为 per-app 后: - -- 新增模型 B 不再引起模型 A 的 replica 重新调度 → **A 的 GPU/内存占用更稳定** -- 删除模型 B 也不会触发 A 的滚动更新 - -但仍需注意: - -- 如果 Ray 集群发生节点故障/资源回收,Serve 本身仍可能重启个别 replica(这是系统层行为) - ---- - -## 验证与验收流程(建议) - -### A. 功能验收(API/UI) - -1. 通过 UI/或 API 创建模型 A,等待 RUNNING -2. 记录 A 的: - - `model_key_A` - - `endpoint.openai_base_url_A` -3. 再创建模型 B,等待 RUNNING -4. 确认: - - A 的 endpoint 仍可用(对 A 的 base_url 发 chat completion) - - B 的 endpoint 可用 -5. 删除模型 B,确认: - - B endpoint 404/不可用 - - A endpoint 仍可用 - -### B. “不影响其它模型”的强验证(Ray actor 级别) - -在 Ray 上抓取 A 对应 `LLMServer` replica 的 actor_id/node_id: - -- 创建 B 前:`actor_id_A_before` -- 创建 B 后:`actor_id_A_after` -- 删除 B 后:`actor_id_A_after_delete` - -预期: - -- `actor_id_A_before == actor_id_A_after == actor_id_A_after_delete` - -(允许 `LLMRouter` 变化,但 **LLMServer for A 不应变化**) - ---- - -## 需要修改的代码点(清单级) - -> 这里只列“改哪里”,不展开具体实现(实现时按 TDD 补单测)。 - -- `argus.service.serving_reconciler`: - - 从“全量 apply 单 app”改为“按 model_key 局部 apply/delete 单 app” - - GPU 预检查改为 per-model -- `argus.service.serve_client`: - - 增加 `delete_app(app_name)`(封装 `serve.delete(app_name)`) - - `apply_app` 传入 `app_name/route_prefix`(已存在,但将不再传固定 app_name) -- `argus.service.app`(Serving API 输出): - - `_serve_model_public().endpoint.openai_base_url` 改为 per-model 前缀 - - `/api/v2/serve/models` list/get 的 openai_base_url 语义调整(可返回“该模型的 base_url”,列表里每条都有) -- `argus.service.ui`(Serving 页面): - - “OpenAI /v1/models” 需要调整为“选择某个模型后打开该模型的 /v1/models” - - 详情页 curl 示例使用 per-model base_url - diff --git a/specs/mvp/v3.8/v3.8_per_model_app_dev_plan.md b/specs/mvp/v3.8/v3.8_per_model_app_dev_plan.md deleted file mode 100644 index e61b579..0000000 --- a/specs/mvp/v3.8/v3.8_per_model_app_dev_plan.md +++ /dev/null @@ -1,174 +0,0 @@ -# MVP v3.8(变更)开发计划:Per-Model Serve App(TDD) - -> 目标:按 `specs/mvp/v3.8/v3.8_per_model_app.md` 将 v3.8 从“单 app 多模型(全量重建)”改为“**每个模型一个 Ray Serve app + 独立 route_prefix**”,实现增删/缩放某个模型不触发其它模型重启与重调度。 - -## 约束与结论 - -- 推理端口固定:`8000` -- 推理 endpoint **不做鉴权** -- `model_id` 前缀规则:`--` -- `LLMConfig.accelerator_type` 由 `configs/dev.yaml` 决定(dev/h1: `H20`) -- 路由方案(本迭代固定): - - `app_name = model_key` - - `route_prefix = /serve/` - - `openai_base_url = http://:8000/serve//v1` - -## 非目标(明确不做) - -- 不提供统一 `/v1` 的“跨模型聚合路由”(如要,需要额外 router 层;可在后续迭代做) -- 不改 ServingSpec 语义(输入仍为 `model_id/model_source/num_replicas/gpus_per_replica/engine_kwargs`) - ---- - -## M0 - 基线回归与分支保护 - -**目的**:确保切换架构前训练/现有功能不回退。 - -### 测试 -- [ ] 本地:`.venv/bin/python -m pytest` 全绿(coverage ≥ 90%) - -### 验收 -- [ ] 基线可用;进入 M1 - ---- - -## M1 - API 输出与 endpoint 语义调整(单测驱动) - -**目的**:API/DB/前端都统一 per-model 的 `openai_base_url` 语义;避免 UI/脚本继续使用 `/v1` 根路径。 - -### 变更点 -- `GET /api/v2/serve/models`: - - 保持返回 `items[]`,但每个 item 的 `endpoint.openai_base_url` 必须是 per-model base url - - `openai_base_url`(列表层级字段)处理策略二选一: - - A(推荐):移除该字段(breaking,需同步 UI/脚本) - - B(兼容):保留但改为 `null` 或提示字符串(不再保证可用) -- `GET /api/v2/serve/models/{model_key}`: - - `model.endpoint.openai_base_url` 改为 per-model base url - -### 单测(先写) -- [ ] 更新/新增 `src/mvp/py/tests/test_app_serving_api.py` - - 断言 `endpoint.openai_base_url` 包含 `/serve//v1` - - 断言多条 models 的 base_url 不相同(随 model_key 变化) - -### 实现 -- [ ] 更新 `src/mvp/py/argus/service/app.py`: - - `_serve_model_public()` 的 `endpoint.openai_base_url` 拼接 per-model prefix - - 如选择移除/调整 list 层的 `openai_base_url`,同步实现 - -### 验收 -- [ ] API 单测通过;返回结构可被 UI/脚本消费 - ---- - -## M2 - ServeClient 扩展(delete_app)+ Reconciler 改造成 per-model(单测驱动) - -**目的**:核心行为变更:每次 tick 只部署/删除一个模型对应的 app,不重建全量 app。 - -### 变更点(行为) -- `QUEUED`: - - 对该 `model_key` 执行 `serve.run(app, name=model_key, route_prefix=/serve/)` - - 状态:`DEPLOYING → RUNNING` -- `DELETING`: - - 对该 `model_key` 执行 `serve.delete(model_key)` - - 状态:`DELETED` -- 资源预检查从“全量 needed_total_gpus”改为“本次变更模型所需 GPU” - -### 单测(先写) -- [ ] 更新 `src/mvp/py/tests/test_serving_reconciler.py` - - `create A` 后,reconciler 只 `apply_app(app_name=A.model_key, route_prefix=/serve/A)` - - `create B` 后,reconciler 只 `apply_app(app_name=B.model_key, route_prefix=/serve/B)`(不再对 A 触发 apply) - - `delete B` 后,reconciler 只 `delete_app(B.model_key)`(不触发 A) - - GPU 不足时:保持 `QUEUED` 且 event=SERVE_PENDING_RESOURCES - -### 实现 -- [ ] `src/mvp/py/argus/service/serve_client.py` - - 增加 `delete_app(app_name: str)`(封装 `serve.delete`) -- [ ] `src/mvp/py/argus/service/serving_reconciler.py` - - 移除“全量 app apply”逻辑 - - 每个 model_key 独立部署:`app_name=model_key`、`route_prefix=/serve/` - - 删除路径走 `delete_app` - -### 验收 -- [ ] reconciler 单测全绿;逻辑可解释(events/state 正确) - ---- - -## M3 - WebUI Serving 页面适配 per-model base_url(单测驱动) - -**目的**:用户从 UI 复制的示例命令必须可用;不再指向根 `/v1`。 - -### 变更点 -- `/ui/serving` 列表: - - “OpenAI /v1/models”按钮改为: - - A:移除(因为没有聚合 `/v1/models`) - - B:保留但文案改为“OpenAI base 需进入详情页” -- `/ui/serving/{model_key}` 详情页: - - `curl` 示例使用 per-model `openai_base_url` - - 增加一键打开:`/serve//v1/models` - -### 单测(先写) -- [ ] 更新/新增 `src/mvp/py/tests/test_ui_serving.py` - - 断言页面包含 `/serve/` 前缀 - - 断言详情页示例里包含 `/serve//v1/chat/completions` - -### 实现 -- [ ] `src/mvp/py/argus/service/ui.py` 更新 Serving UI - -### 验收 -- [ ] UI 单测全绿;页面内容与 API 语义一致 - ---- - -## M4 - E2E 脚本更新(v3.8 serving) - -**目的**:在 dev/h1 一键验证 per-model app:A/B 增删不互相影响,且推理可用。 - -### 变更点 -- 更新 `src/mvp/scripts/run_all_v38_serving.sh`: - - `/v1/models` 与 `chat/completions` 均改用 per-model base_url(`/serve//v1`) - - 增加“隔离验证”步骤: - - 部署 A → 记录 A 的 serve replica actor_id/node_id - - 部署 B → 再次记录 A 的 actor_id/node_id,必须一致 - - 删除 B → 再次记录 A 的 actor_id/node_id,必须一致 - - 最后删除 A - -### 验收 -- [ ] E2E 脚本能跑通且输出明确断言(一致/不一致) - ---- - -## M5 - h1 端到端验证与回归 - -**目的**:确认实际 Ray Serve 行为满足“其它模型不滚动更新”的核心目标。 - -### 操作 -- [ ] 同步代码到:`argus@h1:/home2/argus/infra/mvp/src/mvp` -- [ ] 重启 API:`scripts/61_stop_api.sh && scripts/60_start_api.sh` -- [ ] 执行:`MVP_INTERNAL_TOKEN=... scripts/run_all_v38_serving.sh` - -### 验收标准(必须满足) -- [ ] 新增/删除 B 时,A 的 `LLMServer` replica actor_id/node_id 不变 -- [ ] A/B 的 OpenAI endpoint 均可完成 `chat/completions` -- [ ] 删除 B 后 A 仍可推理 - ---- - -## M6 - 文档与迁移说明 - -**目的**:明确“路由语义变化”和“如何使用”。 - -- [ ] 更新 `src/mvp/README.md`: - - 新增 per-model base_url 说明(`/serve//v1`) - - 提示不再提供聚合 `/v1/models` -- [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`: - - 记录 per-model app 变更与验收结论 - ---- - -## 风险与缓解 - -- **风险:旧 `argus_llm_app` 残留** - - 缓解:在 E2E/迁移步骤里增加一次 best-effort `serve.delete("argus_llm_app")`(可选) -- **风险:用户仍按旧方式访问 `/v1`** - - 缓解:UI/文档/脚本统一切换到 per-model base_url,并在列表页给出明显提示 - diff --git a/specs/mvp/v3.8/v3.8_progress.md b/specs/mvp/v3.8/v3.8_progress.md deleted file mode 100644 index 83eae88..0000000 --- a/specs/mvp/v3.8/v3.8_progress.md +++ /dev/null @@ -1,48 +0,0 @@ -# MVP v3.8 进展记录 - -## 2026-01-06 - -- 完成 v3.8 设计文档:`specs/mvp/v3.8/v3.8_design.md` -- 完成 v3.8 Serving API reference:`specs/mvp/v3.8/v3.8_api.md` -- 完成 v3.8 TDD 开发计划:`specs/mvp/v3.8/v3.8_dev_plan.md` -- 完成 M0:`configs/dev.yaml` 增加 `serving` 配置(http_port=8000, proxy_location=HeadOnly, accelerator_type=H20) -- 完成 M1:ServingSpec 解析/宏替换/路径校验 + 单测(`src/mvp/py/argus/service/serving_spec.py`) -- 完成 M2:SQLite 新增 `serve_models`/`serve_events` + Db API + 单测(`src/mvp/py/argus/service/db.py`) -- 完成 M3:FastAPI Serving 管理 API + 单测(`src/mvp/py/argus/service/app.py`) -- 完成 M4:ServeClient 抽象 + LLMConfig builder(dict 形态)+ 单测(`src/mvp/py/argus/service/serve_client.py`、`src/mvp/py/argus/service/serve_llm_config.py`) -- 完成 M5:Serving reconciler(状态机 + 资源预检查 + mock 单测)(`src/mvp/py/argus/service/serving_reconciler.py`) - -### M6(h1 真实集成) - -- `argus-ray-node` 镜像补齐依赖:`ray[serve,llm]` + `gymnasium` + `dm-tree`(避免 `ray.serve.llm` 导入失败) -- 修复 Ray 2.49.2 兼容性问题: - - `LLMConfig` 不支持 `placement_group_config`,改为使用 `resources_per_bundle`(`src/mvp/py/argus/service/serve_llm_config.py`) -- 远端 E2E: - - `scripts/run_all_v38_serving.sh` 可跑通:create → RUNNING → `/v1/models` → `chat/completions` → delete → DELETED - - 修复脚本中 `/v1/models` 解析的 bash heredoc 引号错误(`src/mvp/scripts/run_all_v38_serving.sh`) - -### M7(WebUI - Serving) - -- WebUI 增加 Serving 页面: - - 列表:`/ui/serving` - - 创建:`/ui/serving/new` - - 详情/事件/缩放/删除:`/ui/serving/{model_key}` -- 单测覆盖: - - `src/mvp/py/tests/test_ui_serving.py` - -### M8(文档/验收) - -- `src/mvp/README.md` 补充 v3.8 serving 端口与 E2E 脚本说明 - -### 环境探测(h1 / head 容器) - -> 目的:确认 Ray Serve LLM 依赖是否开箱即用,避免后续集成阶段才暴雷。 - -- `ray`:可用,版本 `2.49.2` -- `ray.serve`:可 import(Serve 基础可用) -- `ray.serve.llm`:当前不可 import - - 报错:`ModuleNotFoundError: No module named 'gymnasium'` - - 原因:`ray.serve.llm` 的导入链路会触发 `ray.rllib`,而 rllib 依赖 `gymnasium` - -结论: -- v3.8 在实现阶段需要在 `argus-ray-node` 镜像中补齐 `ray[llm]`(推荐)或至少补齐 `gymnasium` 等必要依赖,确保 `from ray.serve.llm import ...` 可用。 diff --git a/specs/mvp/v3.9/ui_refactor_plan.md b/specs/mvp/v3.9/ui_refactor_plan.md deleted file mode 100644 index 2dc3522..0000000 --- a/specs/mvp/v3.9/ui_refactor_plan.md +++ /dev/null @@ -1,108 +0,0 @@ -# v3.9 UI 重构方案(保持功能不变) - -## 背景与问题 - -当前 `src/mvp/py/argus/service/ui.py` 单文件约 1400+ 行,包含: - -- 全局 CSS/JS(长字符串) -- 布局渲染(nav/page 拼接) -- 11 个页面的 HTML + 大段内嵌 JS(包含 TaskSpec 模板与表单逻辑) - -导致:变更难定位、合并冲突多、缺少模块边界、复用困难、测试覆盖薄弱。 - -## 目标(功能不变) - -- **路由与页面行为完全不变**:URL、返回内容、按钮/表单行为、localStorage key(`mvp_token`/`mvp_sftp_password`)、API 调用路径保持不变。 -- **不引入前端构建链/新依赖**(仍然用纯字符串/轻量模板函数)。 -- 将 UI 拆分为可维护的多个文件(放到 `src/mvp/py/argus/ui/`)。 -- 增加最小的单测(确保路由可访问、关键 DOM 标识存在)。 - -## 非目标 - -- 不重做 UI 样式/交互;不引入 React/Vue;不改后端 API。 -- 不新增鉴权逻辑(仍然是浏览器 localStorage + Bearer token)。 - -## 拆分后的目录结构(建议) - -新增包:`src/mvp/py/argus/ui/` - -``` -argus/ui/ - __init__.py # register_ui_routes(app) 统一入口 - assets/ - base_css.py # BASE_CSS 常量 - base_js.py # BASE_JS 常量(apiFetch/apiJson 等通用函数) - layout/ - nav.py # nav(active) + 链接配置 - page.py # page(title, active, body, script, extra_head=...) - pages/ - login.py # /ui/login - tasks.py # /ui/tasks - task_new.py # /ui/tasks/new(模板常量 + 表单 JS) - task_detail.py # /ui/tasks/{task_id} - task_logs.py # /ui/tasks/{task_id}/logs - serving.py # /ui/serving, /ui/serving/new, /ui/serving/{model_key} - data.py # /ui/data - admin.py # /ui/admin - routes.py # 将各 pages.register(app) 聚合注册 -``` - -兼容层(可选但推荐):保留 `src/mvp/py/argus/service/ui.py` 仅做转发: - -```py -from argus.ui import register_ui_routes -``` - -这样可以避免一次性改动 `service/app.py` 的 import 路径,减少风险。 - -## 页面拆分原则 - -每个 page 模块提供两个函数: - -- `render(...) -> HTMLResponse`:只负责拼接 body/script(不直接碰 FastAPI app)。 -- `register(app: FastAPI) -> None`:只负责挂载路由(`@app.get(...)`)。 - -通用能力下沉: - -- `_BASE_CSS`/`_BASE_JS` 移到 `assets/`。 -- `_nav()`、`_page()` 移到 `layout/`。 -- 大块常量(TaskSpec 模板、UI 文案)放在页面模块同文件顶部,避免散落在函数内部。 - -## 资源交付方式(两种可选) - -### 方案 A(最稳):继续内联 CSS/JS,但拆到不同 Python 文件 - -- `page()` 内继续 ``、``。 -- 只改变代码组织,不改变浏览器加载方式,风险最低。 - -### 方案 B(推荐中期):新增静态端点分发资源 - -新增: -- `GET /ui/assets/base.css` -- `GET /ui/assets/base.js` - -页面改为 `` + ``。 -优点:减少 HTML 体积、浏览器缓存更好;缺点:需要确认反向代理/中间件不拦截这些路由。 - -建议 v3.9 先落地方案 A,稳定后再做方案 B。 - -## 迁移步骤(建议分 3 次 PR) - -1) **抽公共层**:引入 `argus/ui/assets/*`、`argus/ui/layout/*`,保持 UI 输出完全一致;`service/ui.py` 仍在但内部改为调用新 layout(或先不动)。 -2) **按页面迁移**:逐个把 routes 迁移到 `argus/ui/pages/*`,每迁一个页面就加一个最小测试用例(200 + 关键文本存在)。 -3) **清理与稳定**:`service/ui.py` 变为兼容转发;可选引入 `/ui/assets/*` 静态端点(方案 B)。 - -## 测试策略(最小但有效) - -新增 `src/mvp/py/tests/test_ui_pages.py`: - -- 创建 FastAPI app(复用现有测试的 app 初始化方式) -- 请求下列页面,断言 `status_code == 200`: - - `/ui/login`, `/ui/tasks`, `/ui/tasks/new`, `/ui/serving`, `/ui/data`, `/ui/admin` -- 断言响应包含稳定锚点文本(例如 `Argus MVP`, `New Task`, `Tasks`),避免脆弱的全量快照。 - -## 验收标准(Definition of Done) - -- 11 个 `/ui/*` 路由行为与输出不变(人工 smoke + 自动化最小测试)。 -- `src/mvp/py/argus/service/ui.py` 不再包含大段 HTML/JS(仅兼容转发或极薄封装)。 -- 新增/修改 UI 页面不需要触碰 1000+ 行单文件;每页的改动范围限定在对应模块。 diff --git a/src/mvp/configs/dev_v30.yaml b/src/mvp/configs/dev_v30.yaml deleted file mode 100644 index 8be5b4e..0000000 --- a/src/mvp/configs/dev_v30.yaml +++ /dev/null @@ -1,64 +0,0 @@ -ray: - # Ray Job server address (head 容器内视角) - address: "http://127.0.0.1:8265" - - # 共享根路径(容器内统一 /private,对齐生产) - shared_root: "/private" - - # 强制 driver 落 worker(head 不跑训练) - entrypoint_num_cpus: 1 - entrypoint_resources: - worker_node: 1 - - # 所有 job 通用 runtime env - runtime_env: - env_vars: - HF_ENDPOINT: "https://hf-mirror.com" - PYTHONUNBUFFERED: "1" - # v3.7: forbid HuggingFace Hub network access from Ray jobs (use cached snapshots). - HF_HUB_OFFLINE: "1" - # Unify cache dirs so `from_pretrained("org/name")` resolves from the same on-disk cache in offline mode. - HF_HOME: "/private/hf" - HUGGINGFACE_HUB_CACHE: "/private/hf/hub" - TRANSFORMERS_CACHE: "/private/hf/hub" - - # v3.0 先不支持 user code 执行 - user_code_path: "/private/user/code" - -service: - api: - host: "0.0.0.0" - port: 8080 - auth: - token_env: "MVP_INTERNAL_TOKEN" - sqlite: - db_path: "/private/common/db/mvp.sqlite3" - scheduler: - tick_s: 5 - retry_interval_s: 60 - max_running_tasks: 1 - -tracking: - wandb: - enabled: true - # For dev compose, recommend docker bridge gateway + host published port for stability. - base_url: "http://172.22.0.1:8090" - api_key_env: "WANDB_API_KEY" - project_suffix: "_project" - -data: - user_root: "/private/users" - sftpgo: - enabled: true - # Returned to users by GET /api/v2/me. For h1 E2E, usually connect to the host IP. - host: "127.0.0.1" - sftp_port: 2022 - # Admin API base should include /api/v2 (SFTPGo OpenAPI server base). - # From head container, access SFTPGo by service name on the compose network. - admin_api_base: "http://argus-sftpgo:8080/api/v2" - admin_user: "admin" - admin_password_env: "SFTPGO_ADMIN_PASSWORD" - retention: - jobs_trash_after_days: 3 - jobs_purge_after_days: 7 - janitor_interval_s: 3600 diff --git a/src/mvp/v1/README.md b/src/mvp/v1/README.md deleted file mode 100644 index fd92b0a..0000000 --- a/src/mvp/v1/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# MVP V1(Ray + VERL PPO)实验脚本 - -本目录用于在“宿主机 + Docker 容器”环境下,**用宿主机脚本(`docker exec`)**协调启动 Ray 集群,并通过 **`ray job submit`(在 head 提交)**跑通一次 `verl` 的 PPO 训练闭环(`total_epochs=1`),且数据/模型/日志/ckpt 都持久化到宿主机目录。 - -## 1. 运行环境与拓扑 - -### 1.1 依赖 - -- 宿主机:Linux -- 必需工具:`docker`、`docker compose`(Compose v2 插件)、`git` -- GPU:至少 8 张可用 GPU(索引 `0-7`),Docker 的 NVIDIA runtime 可用 - -### 1.2 集群拓扑(3 个容器) - -- `mvp-ray-head`(Ray Head) - - **不挂 GPU**(容器内 `nvidia-smi` 不可用) - - `ray start --head --num-cpus=0 --num-gpus=0`:head 只做控制面,不参与计算调度 - - 暴露 dashboard:宿主机端口 `8265` -- `mvp-ray-worker-0`(Ray Worker) - - 4 GPU:`0,1,2,3` - - `ray start ... --resources='{"worker_node": 100}'` -- `mvp-ray-worker-1`(Ray Worker) - - 4 GPU:`4,5,6,7` - - `ray start ... --resources='{"worker_node": 100}'` - -**关键点:driver 不在 head** - -- 作业通过 head 提交:`ray job submit ...` -- 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 entrypoint/driver 只能调度到 worker(head 没有该资源) - -## 2. 持久化目录(宿主机 <-> 容器) - -在宿主机项目根目录(运行脚本时的 `${PWD}`)下使用 `./shared` 做持久化根目录,并 bind mount 到容器内 `/mnt/shared`: - -- 宿主机:`./shared` -- 容器:`/mnt/shared` - -主要内容: - -- 数据集:`/mnt/shared/datasets/gsm8k/` -- HF 缓存:`/mnt/shared/hf/`(脚本会设置 `HF_HOME`,并尽量幂等跳过重复下载) -- 每个 Ray Job 的输出(按 submission id 分目录): - - `/mnt/shared/jobs//logs/` - - `/mnt/shared/jobs//checkpoints/` - -## 3. 整体流程(代码逻辑) - -脚本都在 `src/mvp/v1/scripts/`,整体顺序如下: - -1) `00_prereq_check.sh` - - 检查 `docker/docker compose/git` -2) `05_ensure_verl_repo.sh` - - 若项目根目录下没有 `./verl`,自动 `git clone https://github.com/volcengine/verl.git` -3) `01_up.sh` - - 创建持久化目录(`./shared/...`) - - `docker compose up -d` 启动 3 个容器 -4) `10_install_verl_editable.sh` - - 在 3 个容器内执行 `pip install -e /workspace/verl`(确保 `python -m verl...` 可用且代码与 `./verl` 同步) -5) `20_start_head.sh` - - 在 `mvp-ray-head` 内启动 Ray head(CPU=0、GPU=0) -6) `21_start_workers.sh` - - 在两个 worker 内启动 Ray worker 加入集群 - - 同时给 worker 打 `worker_node` 自定义资源标签 -7) `30_prepare_data_and_model.sh` - - 数据集:若 `train.parquet/test.parquet` 已存在则跳过,否则生成 - - 模型:使用 HF cache(`HF_HOME=/mnt/shared/hf`),存在则跳过,不存在才下载 -8) `40_submit_ppo_epoch1.sh` - - 在 head 容器里执行 `ray job submit` - - 显式指定 `--submission-id=$SUBMISSION_ID` - - 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 driver 在 worker - - 训练参数: - - `trainer.total_epochs=1` - - `trainer.total_training_steps=29`(GSM8K 该配置下对应 29 steps) - - `trainer.save_freq=10`(每 10 step 保存一次 checkpoint,避免磁盘爆炸) - - `trainer.default_local_dir=/mnt/shared/jobs/$SUBMISSION_ID/checkpoints` - - `hydra.run.dir=/mnt/shared/jobs/$SUBMISSION_ID/logs/hydra` -9) `50_status.sh` - - 打印 `ray status` / `ray job list` / `ray job status` / `ray job logs | tail` - -## 4. 运行方法 - -### 4.1 一键执行 - -在项目根目录执行: - -- `./src/mvp/v1/scripts/run_all.sh` - -### 4.2 分步执行(推荐) - -按顺序执行: - -- `./src/mvp/v1/scripts/01_up.sh` -- `./src/mvp/v1/scripts/10_install_verl_editable.sh` -- `./src/mvp/v1/scripts/20_start_head.sh` -- `./src/mvp/v1/scripts/21_start_workers.sh` -- `./src/mvp/v1/scripts/30_prepare_data_and_model.sh` -- `SUBMISSION_ID=ppo_h20_8g_$(date +%Y%m%d_%H%M%S) ./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh` -- `./src/mvp/v1/scripts/50_status.sh` - -### 4.3 查看与停止 - -- Dashboard:`http://<宿主机IP>:8265` -- 列出作业(在 head 容器内): - - `docker exec mvp-ray-head bash -lc "ray job list --address=http://127.0.0.1:8265"` -- 停止某个 submission id: - - `docker exec mvp-ray-head bash -lc "ray job stop --address=http://127.0.0.1:8265 "` - -### 4.4 清理 - -- 停止并删除容器:`./src/mvp/v1/scripts/02_down.sh` -- 清理输出(谨慎,数据量可能很大):删除 `./shared/jobs//` - -## 5. 常见坑 - -- **不传 `--submission-id` 会导致“输出目录难以等于 submission id”**:因为 hydra/ckpt 目录需要在提交前确定。当前脚本会显式传 `--submission-id=$SUBMISSION_ID`,并使用同名目录。 -- **checkpoint 太大**:PPO 的 checkpoint 非常占空间。当前脚本默认 `save_freq=10`,如仍过大,可调大 `save_freq` 或减少保存内容/频率。 - -更多分步操作与验收标准见:`specs/mvp/v1_action.md` diff --git a/src/mvp/v1/arch.excalidraw b/src/mvp/v1/arch.excalidraw deleted file mode 100644 index b9e50ce..0000000 --- a/src/mvp/v1/arch.excalidraw +++ /dev/null @@ -1,1877 +0,0 @@ -{ - "type": "excalidraw", - "version": 2, - "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", - "elements": [ - { - "id": "9Ql3xet2wP4Oy9RJ4s-8H", - "type": "rectangle", - "x": 165.33361053466797, - "y": 124.66671752929688, - "width": 201.66666412353516, - "height": 144.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a0", - "roundness": { - "type": 3 - }, - "seed": 1490759950, - "version": 404, - "versionNonce": 1106431423, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "Yx_fL1nYHbxINMBHYImpi" - }, - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow" - }, - { - "id": "Qzy3gfKzdZyrTwzOpuh86", - "type": "arrow" - }, - { - "id": "zj5n4D3014Kl-BdrNuRZp", - "type": "arrow" - }, - { - "id": "cqQ2Ij98wYdbKqXQcPhxe", - "type": "arrow" - } - ], - "updated": 1766373694946, - "link": null, - "locked": false - }, - { - "id": "Yx_fL1nYHbxINMBHYImpi", - "type": "text", - "x": 232.60698318481445, - "y": 184.5000457763672, - "width": 67.11991882324219, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a1", - "roundness": null, - "seed": 122704078, - "version": 259, - "versionNonce": 104768575, - "isDeleted": false, - "boundElements": [], - "updated": 1766372702157, - "link": null, - "locked": false, - "text": "scripts", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "9Ql3xet2wP4Oy9RJ4s-8H", - "originalText": "scripts", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "1BW7RJyVw0yjg93vAIlWT", - "type": "rectangle", - "x": 813.8334465026855, - "y": 81.00010681152344, - "width": 159.9999771118164, - "height": 88.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a2", - "roundness": { - "type": 3 - }, - "seed": 929976718, - "version": 789, - "versionNonce": 761000657, - "isDeleted": false, - "boundElements": [ - { - "id": "qcyVAzD12V9EAxE15fwAq", - "type": "arrow" - }, - { - "id": "Qzy3gfKzdZyrTwzOpuh86", - "type": "arrow" - } - ], - "updated": 1766373762851, - "link": null, - "locked": false - }, - { - "id": "nIO4qa9Aj4LF_WlHt2BNO", - "type": "rectangle", - "x": 455.8334159851074, - "y": 125.66667175292969, - "width": 201.66666412353516, - "height": 144.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a4", - "roundness": { - "type": 3 - }, - "seed": 2137052434, - "version": 311, - "versionNonce": 1823398289, - "isDeleted": false, - "boundElements": [ - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow" - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow" - }, - { - "id": "qcyVAzD12V9EAxE15fwAq", - "type": "arrow" - } - ], - "updated": 1766373745383, - "link": null, - "locked": false - }, - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow", - "x": 372.0002746582031, - "y": 196.9000457763672, - "width": 252.06685104370115, - "height": 90.56665649414063, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a6", - "roundness": null, - "seed": 1158070290, - "version": 571, - "versionNonce": 231145393, - "isDeleted": false, - "boundElements": [], - "updated": 1766373712586, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 114.08342552185059, - 0 - ], - [ - 114.08342552185059, - -90.56665649414063 - ], - [ - 252.06685104370115, - -90.56665649414063 - ], - [ - 252.06685104370115, - -55.56665649414063 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9Ql3xet2wP4Oy9RJ4s-8H", - "fixedPoint": [ - 1.0247933887424108, - 0.4993087557117625 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "e58le7CfnSkWVb-LoMmj4", - "fixedPoint": [ - 0.49736842105263096, - -0.13157894736842105 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "aBQ9npStDj077n0B7_JZ3", - "type": "rectangle", - "x": 814.000072479248, - "y": 227.6667022705078, - "width": 159.9999771118164, - "height": 88.66665649414062, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "a7", - "roundness": { - "type": 3 - }, - "seed": 1963206674, - "version": 814, - "versionNonce": 371608031, - "isDeleted": false, - "boundElements": [ - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow" - }, - { - "id": "zj5n4D3014Kl-BdrNuRZp", - "type": "arrow" - } - ], - "updated": 1766373791812, - "link": null, - "locked": false - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow", - "x": 648.1671257019043, - "y": 165.23338928222657, - "width": 145.8328742980957, - "height": 121.4333282470703, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aC", - "roundness": null, - "seed": 990553042, - "version": 371, - "versionNonce": 1356695409, - "isDeleted": false, - "boundElements": [], - "updated": 1766373712586, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 80.41647338867188, - 0 - ], - [ - 80.41647338867188, - 121.4333282470703 - ], - [ - 145.8328742980957, - 121.4333282470703 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "e58le7CfnSkWVb-LoMmj4", - "fixedPoint": [ - 1.131578947368421, - 0.49736842105263174 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "aBQ9npStDj077n0B7_JZ3", - "fixedPoint": [ - -0.12500047087676108, - 0.66541378226761 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "atIqduRWNX97Sp6yKe4hj", - "type": "rectangle", - "x": 431.1368826709601, - "y": 26.199444213977472, - "width": 611.63434968004, - "height": 645.8789403469411, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aw", - "roundness": { - "type": 3 - }, - "seed": 311282494, - "version": 376, - "versionNonce": 190431967, - "isDeleted": false, - "boundElements": [], - "updated": 1766373009377, - "link": null, - "locked": false - }, - { - "id": "k6SXmpRXbeQSbRrXG_o6a", - "type": "text", - "x": 656.1800675420305, - "y": -12.200757945542989, - "width": 199.19985961914062, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ax", - "roundness": null, - "seed": 570087970, - "version": 192, - "versionNonce": 158307807, - "isDeleted": false, - "boundElements": [], - "updated": 1766373821110, - "link": null, - "locked": false, - "text": "a single H20 machine", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "a single H20 machine", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "qcyVAzD12V9EAxE15fwAq", - "type": "arrow", - "x": 648.1671257019043, - "y": 165.23338928222657, - "width": 160.66632080078125, - "height": 39.99995422363281, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b03", - "roundness": null, - "seed": 696982833, - "version": 370, - "versionNonce": 1628299665, - "isDeleted": false, - "boundElements": null, - "updated": 1766373712586, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 80.33316040039062, - 0 - ], - [ - 80.33316040039062, - -39.99995422363281 - ], - [ - 160.66632080078125, - -39.99995422363281 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "e58le7CfnSkWVb-LoMmj4", - "fixedPoint": [ - 1.131578947368421, - 0.49736842105263174 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "1BW7RJyVw0yjg93vAIlWT", - "fixedPoint": [ - -0.031250004470349, - 0.4988721803217357 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "Qzy3gfKzdZyrTwzOpuh86", - "type": "arrow", - "x": 371.8835678755345, - "y": 173.74970208077258, - "width": 520.5167182267604, - "height": 132.74959526924914, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b04", - "roundness": null, - "seed": 626515153, - "version": 428, - "versionNonce": 451690431, - "isDeleted": false, - "boundElements": null, - "updated": 1766372702159, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 21.28340523847919, - 0 - ], - [ - 21.28340523847919, - -132.74959526924914 - ], - [ - 520.5167182267604, - -132.74959526924914 - ], - [ - 520.5167182267604, - -111.41628228096789 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9Ql3xet2wP4Oy9RJ4s-8H", - "fixedPoint": [ - 1.024214677416095, - 0.3392833272086004 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "eeIv7mr_n3Lg6B3DlXJHR", - "fixedPoint": [ - 0.49736842105263096, - -0.13157894736842105 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": [ - { - "index": 2, - "start": [ - 21.28340523847919, - 0 - ], - "end": [ - 21.28340523847919, - -132.74959526924914 - ] - } - ], - "startIsSpecial": false, - "endIsSpecial": false - }, - { - "id": "zj5n4D3014Kl-BdrNuRZp", - "type": "arrow", - "x": 371.87357276423296, - "y": 221.26066759479318, - "width": 525.8600263263431, - "height": 135.07269116985526, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b05", - "roundness": null, - "seed": 2137319889, - "version": 378, - "versionNonce": 1170250751, - "isDeleted": false, - "boundElements": null, - "updated": 1766372702159, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 35.29340034978071, - 0 - ], - [ - 35.29340034978071, - 135.07269116985526 - ], - [ - 525.8600263263431, - 135.07269116985526 - ], - [ - 525.8600263263431, - 116.40612622844901 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9Ql3xet2wP4Oy9RJ4s-8H", - "fixedPoint": [ - 1.0241651148800903, - 0.6677001626107852 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "c4sciwjA9ZH_GjmspcBan", - "fixedPoint": [ - 0.49736842105262796, - 1.131578947368421 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": [ - { - "index": 2, - "start": [ - 35.29340034978071, - 0 - ], - "end": [ - 35.29340034978071, - 135.07269116985526 - ] - } - ], - "startIsSpecial": false, - "endIsSpecial": false - }, - { - "id": "bzQAQDSv4fPQAKJkqw9an", - "type": "arrow", - "x": 287.8335380554199, - "y": -76.33323669433594, - "width": 70.00006103515625, - "height": 26, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b06", - "roundness": null, - "seed": 301869471, - "version": 103, - "versionNonce": 2132861809, - "isDeleted": false, - "boundElements": null, - "updated": 1766372404307, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 35.000030517578125, - 0 - ], - [ - 35.000030517578125, - -26 - ], - [ - 70.00006103515625, - -26 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "JoAr6EMunnvVMp7Uo00g5", - "type": "text", - "x": 379.1668510437012, - "y": -102.99992370605469, - "width": 165.33987426757812, - "height": 25, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b07", - "roundness": null, - "seed": 1372433471, - "version": 19, - "versionNonce": 1494393073, - "isDeleted": false, - "boundElements": null, - "updated": 1766372417246, - "link": null, - "locked": false, - "text": "start ray cluster", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "start ray cluster", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "mWLybQDTFo_Um3eT0cEDa", - "type": "arrow", - "x": 577.8335380554199, - "y": -84.33323669433594, - "width": 59.33331298828125, - "height": 24.66668701171875, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b08", - "roundness": null, - "seed": 2218161, - "version": 62, - "versionNonce": 49511103, - "isDeleted": false, - "boundElements": null, - "updated": 1766372425873, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 29.666656494140625, - 0 - ], - [ - 29.666656494140625, - -24.66668701171875 - ], - [ - 59.33331298828125, - -24.66668701171875 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": null, - "startIsSpecial": null, - "endIsSpecial": null - }, - { - "id": "sy0AQFPiKMUpC_fNA1u5Y", - "type": "text", - "x": 653.8335380554199, - "y": -102.99992370605469, - "width": 312.499755859375, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b09", - "roundness": null, - "seed": 516560529, - "version": 57, - "versionNonce": 1162137087, - "isDeleted": false, - "boundElements": null, - "updated": 1766372458567, - "link": null, - "locked": false, - "text": "ray job submit / drivier - worker", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "ray job submit / drivier - worker", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "e58le7CfnSkWVb-LoMmj4", - "type": "ellipse", - "x": 605.1671257019043, - "y": 146.33338928222656, - "width": 38, - "height": 38, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#99e9f2", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0C", - "roundness": { - "type": 2 - }, - "seed": 915719889, - "version": 349, - "versionNonce": 1238458833, - "isDeleted": false, - "boundElements": [ - { - "id": "bAFwE3wo46b9BFt_2Gnm2", - "type": "arrow" - }, - { - "id": "qcyVAzD12V9EAxE15fwAq", - "type": "arrow" - }, - { - "id": "QgI8Gn67BTTI1MdOhOjjn", - "type": "arrow" - }, - { - "id": "cqQ2Ij98wYdbKqXQcPhxe", - "type": "arrow" - } - ], - "updated": 1766373712585, - "link": null, - "locked": false - }, - { - "id": "eeIv7mr_n3Lg6B3DlXJHR", - "type": "ellipse", - "x": 873.5002861022949, - "y": 67.33341979980469, - "width": 38, - "height": 38, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0D", - "roundness": { - "type": 2 - }, - "seed": 1674226303, - "version": 428, - "versionNonce": 1555106719, - "isDeleted": false, - "boundElements": [ - { - "id": "Qzy3gfKzdZyrTwzOpuh86", - "type": "arrow" - } - ], - "updated": 1766372702159, - "link": null, - "locked": false - }, - { - "id": "c4sciwjA9ZH_GjmspcBan", - "type": "ellipse", - "x": 878.8335990905762, - "y": 294.6667938232422, - "width": 38, - "height": 38, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0E", - "roundness": { - "type": 2 - }, - "seed": 2058462079, - "version": 476, - "versionNonce": 803991519, - "isDeleted": false, - "boundElements": [ - { - "id": "zj5n4D3014Kl-BdrNuRZp", - "type": "arrow" - } - ], - "updated": 1766372702159, - "link": null, - "locked": false - }, - { - "id": "QQdAB0FVqHTGhVCKstHrm", - "type": "ellipse", - "x": 308.1669120788574, - "y": -185.9999237060547, - "width": 38, - "height": 38, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#99e9f2", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0F", - "roundness": { - "type": 2 - }, - "seed": 156527057, - "version": 305, - "versionNonce": 1422415839, - "isDeleted": false, - "boundElements": [], - "updated": 1766372581044, - "link": null, - "locked": false - }, - { - "id": "37ojV_5RQu-H5_oLEw_CC", - "type": "text", - "x": 375.1668510437012, - "y": -176.9999237060547, - "width": 136.61990356445312, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0G", - "roundness": null, - "seed": 122498079, - "version": 15, - "versionNonce": 121837457, - "isDeleted": false, - "boundElements": null, - "updated": 1766372589741, - "link": null, - "locked": false, - "text": "ray head node", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "ray head node", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "ZyQitTq4F7KArc1LsSU0L", - "type": "ellipse", - "x": 578.1668510437012, - "y": -185.33323669433594, - "width": 38, - "height": 38, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0H", - "roundness": { - "type": 2 - }, - "seed": 666768689, - "version": 440, - "versionNonce": 1110150815, - "isDeleted": false, - "boundElements": [], - "updated": 1766372594958, - "link": null, - "locked": false - }, - { - "id": "B_AKAU5jOYUjwmhK54Ddr", - "type": "text", - "x": 657.5235862731934, - "y": -178.16656494140625, - "width": 154.65988159179688, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0I", - "roundness": null, - "seed": 1500425873, - "version": 72, - "versionNonce": 150248991, - "isDeleted": false, - "boundElements": [], - "updated": 1766372606781, - "link": null, - "locked": false, - "text": "ray worker node", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "ray worker node", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "WPOvDTFRcRTqysrgfilw_", - "type": "rectangle", - "x": 453.1667900085449, - "y": 395.0000762939453, - "width": 572.6666870117189, - "height": 251.3333740234375, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0K", - "roundness": null, - "seed": 62506143, - "version": 121, - "versionNonce": 1380555377, - "isDeleted": false, - "boundElements": null, - "updated": 1766373299556, - "link": null, - "locked": false - }, - { - "id": "icDpH3wy-c2_99TXtmru5", - "type": "rectangle", - "x": 469.1667900085449, - "y": 410.87457554413584, - "width": 83.33331298828125, - "height": 138.7922487966845, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0L", - "roundness": null, - "seed": 292536607, - "version": 130, - "versionNonce": 398901169, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "oYnPwXC5ulNTf8ayVoUHX" - } - ], - "updated": 1766376371670, - "link": null, - "locked": false - }, - { - "id": "oYnPwXC5ulNTf8ayVoUHX", - "type": "text", - "x": 476.93347549438477, - "y": 455.2706999424781, - "width": 67.79994201660156, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0M", - "roundness": null, - "seed": 964724095, - "version": 75, - "versionNonce": 1019940831, - "isDeleted": false, - "boundElements": null, - "updated": 1766376371670, - "link": null, - "locked": false, - "text": "datase\nts", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "icDpH3wy-c2_99TXtmru5", - "originalText": "datasets", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "s3rGKdwzC87Z1v9gN0N-5", - "type": "rectangle", - "x": 582.166820526123, - "y": 410.4639351318977, - "width": 83.33331298828125, - "height": 138.7922487966845, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0N", - "roundness": null, - "seed": 401691633, - "version": 190, - "versionNonce": 700169215, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "D2W7CAX99N7IpvqJQP6eX" - } - ], - "updated": 1766376371670, - "link": null, - "locked": false - }, - { - "id": "D2W7CAX99N7IpvqJQP6eX", - "type": "text", - "x": 613.1934852600098, - "y": 467.36005953023994, - "width": 21.279983520507812, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0O", - "roundness": null, - "seed": 325299665, - "version": 138, - "versionNonce": 770525041, - "isDeleted": false, - "boundElements": [], - "updated": 1766376371670, - "link": null, - "locked": false, - "text": "hf", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "s3rGKdwzC87Z1v9gN0N-5", - "originalText": "hf", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "EGfEHSzmWYNx5ec9bP-9v", - "type": "rectangle", - "x": 701.5001945495605, - "y": 409.6427294956321, - "width": 83.33331298828125, - "height": 138.7922487966845, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0P", - "roundness": null, - "seed": 974114385, - "version": 214, - "versionNonce": 2090152273, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "XIfw_ABOr1dlUCUXEBJqA" - } - ], - "updated": 1766376371670, - "link": null, - "locked": false - }, - { - "id": "XIfw_ABOr1dlUCUXEBJqA", - "type": "text", - "x": 722.9068641662598, - "y": 466.53885389397436, - "width": 40.51997375488281, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0Q", - "roundness": null, - "seed": 881056817, - "version": 164, - "versionNonce": 663619647, - "isDeleted": false, - "boundElements": [], - "updated": 1766376371670, - "link": null, - "locked": false, - "text": "jobs", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "EGfEHSzmWYNx5ec9bP-9v", - "originalText": "jobs", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "Fe9YUb1NNu32IhvievWnr", - "type": "rectangle", - "x": 812.8334465026855, - "y": 408.8214486711559, - "width": 83.33331298828125, - "height": 138.7922487966845, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0R", - "roundness": null, - "seed": 66661759, - "version": 268, - "versionNonce": 1178642527, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "UZhPJS_QBKW_LMYwu1enR" - } - ], - "updated": 1766376371670, - "link": null, - "locked": false - }, - { - "id": "UZhPJS_QBKW_LMYwu1enR", - "type": "text", - "x": 821.1101188659668, - "y": 465.71757306949814, - "width": 66.77996826171875, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0S", - "roundness": null, - "seed": 1284842911, - "version": 227, - "versionNonce": 969134353, - "isDeleted": false, - "boundElements": [], - "updated": 1766376371670, - "link": null, - "locked": false, - "text": "output", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Fe9YUb1NNu32IhvievWnr", - "originalText": "output", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "r8maqUMI8CEoTqgUHwtnZ", - "type": "rectangle", - "x": 924.8335075378418, - "y": 408.0001678466797, - "width": 83.33331298828125, - "height": 138.7922487966845, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0T", - "roundness": null, - "seed": 1203268831, - "version": 320, - "versionNonce": 204048113, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "nL0ZOKRltZyanG1aa7hOW" - } - ], - "updated": 1766376371670, - "link": null, - "locked": false - }, - { - "id": "nL0ZOKRltZyanG1aa7hOW", - "type": "text", - "x": 951.7201805114746, - "y": 464.8962922450219, - "width": 29.559967041015625, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0U", - "roundness": null, - "seed": 150305023, - "version": 273, - "versionNonce": 1246452895, - "isDeleted": false, - "boundElements": [], - "updated": 1766376371670, - "link": null, - "locked": false, - "text": "ray", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "r8maqUMI8CEoTqgUHwtnZ", - "originalText": "ray", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "OmfvpsuW0dhubnI5NH1Ni", - "type": "text", - "x": 675.8334770202637, - "y": 593.0000762939453, - "width": 121.67990112304688, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0V", - "roundness": null, - "seed": 1166095967, - "version": 60, - "versionNonce": 643125617, - "isDeleted": false, - "boundElements": null, - "updated": 1766373325167, - "link": null, - "locked": false, - "text": "/mnt/shared", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "/mnt/shared", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "cqQ2Ij98wYdbKqXQcPhxe", - "type": "arrow", - "x": 371.9714008624129, - "y": 208.42023305299114, - "width": 252.09572483949137, - "height": 50.3333740234375, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0W", - "roundness": null, - "seed": 1948514207, - "version": 127, - "versionNonce": 977608497, - "isDeleted": false, - "boundElements": null, - "updated": 1766373713002, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 96.77889477662518, - 0 - ], - [ - 96.77889477662518, - 31.246530252672926 - ], - [ - 252.09572483949137, - 31.246530252672926 - ], - [ - 252.09572483949137, - -19.086843770764574 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9Ql3xet2wP4Oy9RJ4s-8H", - "fixedPoint": [ - 1.0246502128937116, - 0.5789413922556957 - ], - "focus": 0, - "gap": 0 - }, - "endBinding": { - "elementId": "e58le7CfnSkWVb-LoMmj4", - "fixedPoint": [ - 0.49736842105263096, - 1.131578947368421 - ], - "focus": 0, - "gap": 0 - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": true, - "fixedSegments": [ - { - "index": 3, - "start": [ - 96.77889477662518, - 31.246530252672926 - ], - "end": [ - 252.09572483949137, - 31.246530252672926 - ] - } - ], - "startIsSpecial": false, - "endIsSpecial": false - }, - { - "id": "xn_M6D4MoCOvg2I2eSA3o", - "type": "text", - "x": 491.16682052612305, - "y": 237.66676330566406, - "width": 137.33990478515625, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0X", - "roundness": null, - "seed": 485981873, - "version": 51, - "versionNonce": 224706367, - "isDeleted": false, - "boundElements": null, - "updated": 1766373736215, - "link": null, - "locked": false, - "text": "ray job submit", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "ray job submit", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "n0XX1HzCpzHfeo4hTRson", - "type": "text", - "x": 456.5001640319824, - "y": 281.00013732910156, - "width": 196.61985778808594, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0Y", - "roundness": null, - "seed": 960522577, - "version": 46, - "versionNonce": 1758456319, - "isDeleted": false, - "boundElements": null, - "updated": 1766373758962, - "link": null, - "locked": false, - "text": "head node container", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "head node container", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "-rnnyMPU-tQs8UVNBCb7C", - "type": "text", - "x": 792.5001945495605, - "y": 176.33338928222656, - "width": 225.5198211669922, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0Z", - "roundness": null, - "seed": 501756561, - "version": 78, - "versionNonce": 1893991729, - "isDeleted": false, - "boundElements": null, - "updated": 1766373794871, - "link": null, - "locked": false, - "text": "worker node containers\n 4 x H20", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "worker node containers\n 4 x H20", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "qWTdVy6o3A4fkZsGRMCKB", - "type": "text", - "x": 483.4334098815918, - "y": 557.5000762939453, - "width": 40.1335388183593, - "height": 16.722307840983046, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0a", - "roundness": null, - "seed": 438165041, - "version": 42, - "versionNonce": 1297545873, - "isDeleted": false, - "boundElements": null, - "updated": 1766376434059, - "link": null, - "locked": false, - "text": "数据集", - "fontSize": 13.377846272786437, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "数据集", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "Zr04JN1SwCMGrPB7Otn5n", - "type": "text", - "x": 599.7667686462403, - "y": 556.6389223734539, - "width": 53.47998046875, - "height": 16.722307840983046, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0b", - "roundness": null, - "seed": 1427393105, - "version": 108, - "versionNonce": 20002801, - "isDeleted": false, - "boundElements": [], - "updated": 1766376447293, - "link": null, - "locked": false, - "text": "基座模型", - "fontSize": 13.377846272786437, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "基座模型", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "U5mN9zuDxufwaTsXAez1v", - "type": "text", - "x": 683.1001121520997, - "y": 553.9721743265789, - "width": 116.947265625, - "height": 33.44461568196609, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0c", - "roundness": null, - "seed": 1016566161, - "version": 228, - "versionNonce": 460195775, - "isDeleted": false, - "boundElements": [], - "updated": 1766376548390, - "link": null, - "locked": false, - "text": "job级别\nckpt, config, log等", - "fontSize": 13.377846272786437, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "job级别\nckpt, config, log等", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "kyEqz0rZKz404SUTYZRai", - "type": "text", - "x": 931.766707611084, - "y": 557.3056093851726, - "width": 74.01625061035156, - "height": 33.44461568196609, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b0d", - "roundness": null, - "seed": 1742834719, - "version": 152, - "versionNonce": 2136750623, - "isDeleted": false, - "boundElements": [], - "updated": 1766376569303, - "link": null, - "locked": false, - "text": "系统级\nsession日志", - "fontSize": 13.377846272786437, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "系统级\nsession日志", - "autoResize": true, - "lineHeight": 1.25 - } - ], - "appState": { - "gridSize": 20, - "gridStep": 5, - "gridModeEnabled": false, - "viewBackgroundColor": "#ffffff" - }, - "files": {} -} \ No newline at end of file diff --git a/src/mvp/v1/docker-compose.yaml b/src/mvp/v1/docker-compose.yaml deleted file mode 100644 index 5d31a03..0000000 --- a/src/mvp/v1/docker-compose.yaml +++ /dev/null @@ -1,86 +0,0 @@ -version: "3.8" - -services: - ray_head: - image: verlai/verl:sgl055.latest - container_name: mvp-ray-head - command: sleep infinity - ports: - - "8265:8265" - volumes: - - ./verl:/workspace/verl - - ./shared:/mnt/shared - shm_size: "10g" - ulimits: - nofile: - soft: 65536 - hard: 65536 - cap_add: - - SYS_ADMIN - - SYS_PTRACE - networks: - - mvp-ray-net - environment: - HF_HOME: "/mnt/shared/hf" - HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub" - TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers" - HF_ENDPOINT: "https://hf-mirror.com" - PYTHONUNBUFFERED: "1" - - ray_worker_0: - image: verlai/verl:sgl055.latest - container_name: mvp-ray-worker-0 - command: sleep infinity - volumes: - - ./verl:/workspace/verl - - ./shared:/mnt/shared - shm_size: "10g" - ulimits: - nofile: - soft: 65536 - hard: 65536 - cap_add: - - SYS_ADMIN - - SYS_PTRACE - networks: - - mvp-ray-net - runtime: nvidia - environment: - NVIDIA_VISIBLE_DEVICES: "0,1,2,3" - NVIDIA_DRIVER_CAPABILITIES: "all" - HF_HOME: "/mnt/shared/hf" - HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub" - TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers" - HF_ENDPOINT: "https://hf-mirror.com" - PYTHONUNBUFFERED: "1" - - ray_worker_1: - image: verlai/verl:sgl055.latest - container_name: mvp-ray-worker-1 - command: sleep infinity - volumes: - - ./verl:/workspace/verl - - ./shared:/mnt/shared - shm_size: "10g" - ulimits: - nofile: - soft: 65536 - hard: 65536 - cap_add: - - SYS_ADMIN - - SYS_PTRACE - networks: - - mvp-ray-net - runtime: nvidia - environment: - NVIDIA_VISIBLE_DEVICES: "4,5,6,7" - NVIDIA_DRIVER_CAPABILITIES: "all" - HF_HOME: "/mnt/shared/hf" - HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub" - TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers" - HF_ENDPOINT: "https://hf-mirror.com" - PYTHONUNBUFFERED: "1" - -networks: - mvp-ray-net: - driver: bridge diff --git a/src/mvp/v1/scripts/00_prereq_check.sh b/src/mvp/v1/scripts/00_prereq_check.sh deleted file mode 100755 index db093a9..0000000 --- a/src/mvp/v1/scripts/00_prereq_check.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -require_cmd docker - -require_cmd git - -if ! docker compose version >/dev/null 2>&1; then - echo "docker compose plugin not available; please install docker compose v2" >&2 - exit 1 -fi - -echo "OK: docker + docker compose + git" diff --git a/src/mvp/v1/scripts/01_up.sh b/src/mvp/v1/scripts/01_up.sh deleted file mode 100755 index d835ba2..0000000 --- a/src/mvp/v1/scripts/01_up.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -"${SCRIPT_DIR}/00_prereq_check.sh" -"${SCRIPT_DIR}/05_ensure_verl_repo.sh" - -mkdir -p \ - "${ROOT_DIR}/shared/hf" \ - "${ROOT_DIR}/shared/datasets" \ - "${ROOT_DIR}/shared/jobs" \ - "${ROOT_DIR}/shared/outputs" \ - "${ROOT_DIR}/shared/ray" - -dc up -d -dc ps - diff --git a/src/mvp/v1/scripts/02_down.sh b/src/mvp/v1/scripts/02_down.sh deleted file mode 100755 index 750040e..0000000 --- a/src/mvp/v1/scripts/02_down.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -dc down - diff --git a/src/mvp/v1/scripts/05_ensure_verl_repo.sh b/src/mvp/v1/scripts/05_ensure_verl_repo.sh deleted file mode 100755 index 99eacbd..0000000 --- a/src/mvp/v1/scripts/05_ensure_verl_repo.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -VERL_DIR="${ROOT_DIR}/verl" - -if [[ -d "${VERL_DIR}/.git" ]]; then - echo "OK: verl repo exists: ${VERL_DIR}" - exit 0 -fi - -echo "verl repo not found at ${VERL_DIR}; cloning..." -rm -rf "${VERL_DIR}" -git clone https://github.com/volcengine/verl.git "${VERL_DIR}" -echo "OK: cloned verl -> ${VERL_DIR}" - diff --git a/src/mvp/v1/scripts/10_install_verl_editable.sh b/src/mvp/v1/scripts/10_install_verl_editable.sh deleted file mode 100755 index 549e2b2..0000000 --- a/src/mvp/v1/scripts/10_install_verl_editable.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -install_one() { - local name="$1" - echo "[${name}] ensure verl importable" - if ! dexec "${name}" python3 -c "import verl; print(verl.__file__)" >/dev/null 2>&1; then - echo "[${name}] verl not importable; installing editable from /workspace/verl" - dexec "${name}" bash -lc "pip install -e /workspace/verl" - else - echo "[${name}] verl import OK" - fi -} - -install_one "${HEAD_CONTAINER}" -install_one "${WORKER0_CONTAINER}" -install_one "${WORKER1_CONTAINER}" - diff --git a/src/mvp/v1/scripts/20_start_head.sh b/src/mvp/v1/scripts/20_start_head.sh deleted file mode 100755 index e14da5b..0000000 --- a/src/mvp/v1/scripts/20_start_head.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -echo "[head] ray stop (ignore errors)" -dexec "${HEAD_CONTAINER}" bash -lc "ray stop --force || true" - -echo "[head] ray start (CPU=0, GPU=0 to prevent scheduling on head)" -HEAD_IP="$(container_ip "${HEAD_CONTAINER}")" -echo "[head] container ip: ${HEAD_IP}" -dexec "${HEAD_CONTAINER}" bash -lc "ray start --head --node-ip-address=${HEAD_IP} --dashboard-host=0.0.0.0 --dashboard-port=8265 --port=6379 --num-cpus=0 --num-gpus=0" - -echo "[head] ray status" -dexec "${HEAD_CONTAINER}" bash -lc "ray status || true" diff --git a/src/mvp/v1/scripts/21_start_workers.sh b/src/mvp/v1/scripts/21_start_workers.sh deleted file mode 100755 index 041d709..0000000 --- a/src/mvp/v1/scripts/21_start_workers.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -start_worker() { - local name="$1" - local node_ip - node_ip="$(container_ip "${name}")" - echo "[${name}] ray stop (ignore errors)" - dexec "${name}" bash -lc "ray stop --force || true" - - local head_ip - head_ip="$(container_ip "${HEAD_CONTAINER}")" - echo "[${name}] container ip: ${node_ip}" - echo "[${name}] ray start -> join ${head_ip}:6379 (num_gpus=4, resources worker_node=100)" - dexec "${name}" bash -lc "ray start --node-ip-address=${node_ip} --address=${head_ip}:6379 --num-gpus=4 --resources='{\"worker_node\": 100}'" -} - -start_worker "${WORKER0_CONTAINER}" -start_worker "${WORKER1_CONTAINER}" - -echo "[head] waiting for workers to register" -for _ in $(seq 1 30); do - if dexec "${HEAD_CONTAINER}" bash -lc "ray status" | grep -q "Active:"; then - break - fi - sleep 2 -done - -dexec "${HEAD_CONTAINER}" bash -lc "ray status || true" diff --git a/src/mvp/v1/scripts/30_prepare_data_and_model.sh b/src/mvp/v1/scripts/30_prepare_data_and_model.sh deleted file mode 100755 index bff6da6..0000000 --- a/src/mvp/v1/scripts/30_prepare_data_and_model.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -DATA_DIR="/mnt/shared/datasets/gsm8k" -MODEL_ID="Qwen/Qwen2.5-0.5B-Instruct" - -echo "[head] prepare dataset (idempotent) -> ${DATA_DIR}" -dexec "${HEAD_CONTAINER}" bash -lc "mkdir -p ${DATA_DIR} && if [[ -f ${DATA_DIR}/train.parquet && -f ${DATA_DIR}/test.parquet ]]; then echo 'dataset_exists: skip'; else python3 /workspace/verl/examples/data_preprocess/gsm8k.py --local_save_dir ${DATA_DIR}; fi" - -echo "[head] ensure model cached to persistent HF_HOME (idempotent) -> ${MODEL_ID}" -PY_CODE="$(cat <<'PY' -import os - -model_id = os.environ["MODEL_ID"] - -hf_home = os.environ.get("HF_HOME", "/mnt/shared/hf") -os.environ.setdefault("HF_HOME", hf_home) -os.environ.setdefault("HUGGINGFACE_HUB_CACHE", os.path.join(hf_home, "hub")) -os.environ.setdefault("TRANSFORMERS_CACHE", os.path.join(hf_home, "transformers")) - -from huggingface_hub import snapshot_download - -try: - snapshot_download(repo_id=model_id, local_files_only=True) - print("model_cache_exists: skip", model_id) -except Exception: - print("model_cache_missing: downloading", model_id) - snapshot_download(repo_id=model_id) - print("model_cached_ok:", model_id) -PY -)" - -printf "%s\n" "${PY_CODE}" | dexec "${HEAD_CONTAINER}" bash -lc "MODEL_ID='${MODEL_ID}' python3 -" diff --git a/src/mvp/v1/scripts/40_submit_ppo_epoch1.sh b/src/mvp/v1/scripts/40_submit_ppo_epoch1.sh deleted file mode 100755 index 0b62dce..0000000 --- a/src/mvp/v1/scripts/40_submit_ppo_epoch1.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -SUBMISSION_ID="${SUBMISSION_ID:-mvp_ppo_$(timestamp)_$RANDOM}" -# 为了让“输出目录 = submission id”,默认把 JOB_TAG 也设成 SUBMISSION_ID(可手动覆盖)。 -JOB_TAG="${JOB_TAG:-${SUBMISSION_ID}}" -JOB_DIR="/mnt/shared/jobs/${SUBMISSION_ID}" - -MODEL_ID="Qwen/Qwen2.5-0.5B-Instruct" -TRAIN_FILE="/mnt/shared/datasets/gsm8k/train.parquet" -VAL_FILE="/mnt/shared/datasets/gsm8k/test.parquet" - -echo "[head] create job dir: ${JOB_DIR}" -dexec "${HEAD_CONTAINER}" bash -lc "mkdir -p ${JOB_DIR}/logs ${JOB_DIR}/checkpoints ${JOB_DIR}/config" - -SUBMIT_CMD="python3 -m verl.trainer.main_ppo \ -data.train_files=${TRAIN_FILE} \ -data.val_files=${VAL_FILE} \ -data.train_batch_size=256 \ -data.max_prompt_length=512 \ -data.max_response_length=512 \ -actor_rollout_ref.model.path=${MODEL_ID} \ -actor_rollout_ref.actor.optim.lr=1e-6 \ -actor_rollout_ref.actor.ppo_mini_batch_size=64 \ -actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \ -actor_rollout_ref.rollout.name=sglang \ -actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \ -actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ -actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ -actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \ -critic.optim.lr=1e-5 \ -critic.model.path=${MODEL_ID} \ -critic.ppo_micro_batch_size_per_gpu=4 \ -algorithm.kl_ctrl.kl_coef=0.001 \ -trainer.logger=console \ -trainer.val_before_train=False \ -trainer.n_gpus_per_node=4 \ -trainer.nnodes=2 \ -trainer.save_freq=10 \ -trainer.test_freq=29 \ -trainer.total_epochs=1 \ -trainer.total_training_steps=29 \ -trainer.resume_mode=disable \ -trainer.default_local_dir=${JOB_DIR}/checkpoints \ -+ray_kwargs.ray_init.address=auto \ -hydra.run.dir=${JOB_DIR}/logs/hydra" - -printf "%s\n" "${SUBMIT_CMD}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/config/submit_cmd.txt" - -echo "[head] submit PPO via ray job submit (force driver on worker via entrypoint resources)" - -SUBMIT_OUT="$(dexec "${HEAD_CONTAINER}" bash -lc "export HF_HOME=/mnt/shared/hf HUGGINGFACE_HUB_CACHE=/mnt/shared/hf/hub TRANSFORMERS_CACHE=/mnt/shared/hf/transformers HF_ENDPOINT=https://hf-mirror.com PYTHONUNBUFFERED=1; ray job submit --address=http://127.0.0.1:8265 --submission-id='${SUBMISSION_ID}' --entrypoint-num-cpus=1 --entrypoint-resources='{\"worker_node\": 1}' --runtime-env-json='{\"env_vars\":{\"HF_HOME\":\"/mnt/shared/hf\",\"HUGGINGFACE_HUB_CACHE\":\"/mnt/shared/hf/hub\",\"TRANSFORMERS_CACHE\":\"/mnt/shared/hf/transformers\",\"HF_ENDPOINT\":\"https://hf-mirror.com\",\"PYTHONUNBUFFERED\":\"1\"}}' --no-wait -- ${SUBMIT_CMD}")" - -printf "%s\n" "${SUBMIT_OUT}" -printf "%s\n" "${SUBMIT_OUT}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/logs/ray_job_submit.out" - -PARSED_SUBMISSION_ID="$(printf "%s\n" "${SUBMIT_OUT}" | sed -r 's/\x1b\\[[0-9;]*m//g' | grep -Eo "raysubmit_[A-Za-z0-9_-]+" | head -n 1 || true)" -if [[ -n "${PARSED_SUBMISSION_ID}" && "${PARSED_SUBMISSION_ID}" != "${SUBMISSION_ID}" ]]; then - echo "WARN: submission id mismatch: expected=${SUBMISSION_ID} parsed=${PARSED_SUBMISSION_ID}" >&2 -fi - -echo "${SUBMISSION_ID}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/config/ray_submission_id.txt" -echo "ray submission id: ${SUBMISSION_ID}" - -echo "submitted. track via Ray dashboard: http://:8265 (driver should be scheduled on a worker due to entrypoint resources)" -echo "job dir: ${JOB_DIR}" diff --git a/src/mvp/v1/scripts/50_status.sh b/src/mvp/v1/scripts/50_status.sh deleted file mode 100755 index 43bd50b..0000000 --- a/src/mvp/v1/scripts/50_status.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -echo "[head] ray status" -dexec "${HEAD_CONTAINER}" bash -lc "ray status || true" - -echo "[head] ray jobs list (optional)" -dexec "${HEAD_CONTAINER}" bash -lc "ray job list --address=http://127.0.0.1:8265 || true" - -LATEST_JOB_DIR="$(dexec "${HEAD_CONTAINER}" bash -lc "ls -1dt /mnt/shared/jobs/* 2>/dev/null | head -n 1 || true")" -if [[ -n "${LATEST_JOB_DIR}" ]]; then - echo "[host] latest job dir: ${LATEST_JOB_DIR}" - echo "[host] ray submission id (if exists):" - SUB_ID="$(dexec "${HEAD_CONTAINER}" bash -lc "cat ${LATEST_JOB_DIR}/config/ray_submission_id.txt 2>/dev/null || true")" - echo "${SUB_ID}" - if [[ -n "${SUB_ID}" ]]; then - echo "[head] ray job status:" - dexec "${HEAD_CONTAINER}" bash -lc "ray job status --address=http://127.0.0.1:8265 ${SUB_ID} || true" - echo "[head] ray job logs (tail):" - dexec "${HEAD_CONTAINER}" bash -lc "ray job logs --address=http://127.0.0.1:8265 ${SUB_ID} 2>/dev/null | tail -n 60 || true" - fi -fi diff --git a/src/mvp/v1/scripts/lib.sh b/src/mvp/v1/scripts/lib.sh deleted file mode 100755 index 0b34457..0000000 --- a/src/mvp/v1/scripts/lib.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "${SCRIPT_DIR}/../../../../" && pwd)" - -COMPOSE_FILE="${ROOT_DIR}/src/mvp/v1/docker-compose.yaml" - -HEAD_CONTAINER="mvp-ray-head" -WORKER0_CONTAINER="mvp-ray-worker-0" -WORKER1_CONTAINER="mvp-ray-worker-1" - -dc() { - docker compose --project-directory "${ROOT_DIR}" -f "${COMPOSE_FILE}" "$@" -} - -require_cmd() { - local cmd="$1" - command -v "${cmd}" >/dev/null 2>&1 || { - echo "missing required command: ${cmd}" >&2 - exit 1 - } -} - -ensure_container_running() { - local name="$1" - if ! docker ps --format '{{.Names}}' | grep -qx "${name}"; then - echo "container not running: ${name}" >&2 - exit 1 - fi -} - -dexec() { - local name="$1" - shift - ensure_container_running "${name}" - docker exec -i "${name}" "$@" -} - -container_ip() { - local name="$1" - ensure_container_running "${name}" - docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${name}" -} - -timestamp() { - date +"%Y%m%d_%H%M%S" -} diff --git a/src/mvp/v1/scripts/run_all.sh b/src/mvp/v1/scripts/run_all.sh deleted file mode 100755 index 9737409..0000000 --- a/src/mvp/v1/scripts/run_all.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -"${SCRIPT_DIR}/01_up.sh" -"${SCRIPT_DIR}/10_install_verl_editable.sh" -"${SCRIPT_DIR}/20_start_head.sh" -"${SCRIPT_DIR}/21_start_workers.sh" -"${SCRIPT_DIR}/30_prepare_data_and_model.sh" -"${SCRIPT_DIR}/40_submit_ppo_epoch1.sh" -"${SCRIPT_DIR}/50_status.sh" -