LabHub
배우기 러닝패스 코스

AIエージェント — モデルではなくグラフ

保存してあるから、その場所に戻れる

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

체크포인터를 붙인 그래프에서 무엇이 어디에 남는지를 직접 세어 본다. 같은 thread_id 가 이어지는 것을 확인하고, 체크포인트를 목록으로 읽고, 과거 좌표로 되감고, 값을 고쳐 갈래를 치고, 상태를 직렬화해 바이트를 센다. 마지막으로 장부를 새로 만들면 무엇이 사라지는지 재현한다.

왜 중요한가

체크포인터는 단계마다 그 시점의 상태를 통째로 남긴다. 이 한 문장이 좋은 일과 나쁜 일을 동시에 설명한다. 좋은 쪽은 이어서 돌릴 수 있다는 것이다. 여덟 번째 단계에서 실패해도 처음부터 다시 할 필요가 없고, "세 번째에서 다른 자료를 썼으면 어땠을까" 라는 질문에 같은 조건으로 답할 수 있다. 과거 스냅샷의 configinvoke(None, ...) 하면 그 자리에서 다시 돌고, update_state 로 값을 고쳐 넣으면 거기서 갈래가 생긴다. 이때 원래 갈래는 지워지지 않지만 스레드의 "지금" 은 새 갈래로 옮겨 가므로, 원래 결과를 다시 보려면 그 시점의 config 를 손에 들고 있어야 한다. 나쁜 쪽은 저장되는 것이 상태 전체라는 것이다. 상태에 큰 값을 하나 담으면 그 값이 체크포인트 수만큼 되풀이 저장된다. 이 실습에서는 그것을 시간이 아니라 바이트로 잰다 — 시간은 기계와 부하에 따라 달라지지만 크기는 같은 상태면 늘 같기 때문이다. 채점기는 여러분이 적어 둔 설명을 믿지 않습니다. 여러분의 모듈을 실제로 불러 그래프를 돌리고, 체크포인트를 스스로 세어 여러분이 돌려준 값과 대조합니다. 주제와 담는 값의 크기는 실행마다 바뀝니다.

단계

  1. /root/work/agckpt/ckpt.pyANGLES·State·노드 셋(plan·write·reviewbuild_graph(checkpointer=None)·new_saver()·cfg(thread_id) 를 만드세요. 같은 thread_id 는 이어지고 다른 것은 따로 남아야 합니다.
  2. history(app, config)·history_len(app, config)·pending(app, config) 를 더해 체크포인트를 오래된 것부터 읽고 세게 하세요.
  3. snapshot_before(app, config, node) 를 더해 그 노드를 아직 돌지 않은 가장 최근 체크포인트를 찾게 하세요.
  4. replay(app, config, node) 를 더해 그 좌표에서 invoke(None, snapshot.config) 로 다시 돌게 하세요.
  5. fork(app, config, node, values) 를 더해 update_state 로 갈래를 치고, 원래 갈래의 끝도 함께 돌려주게 하세요.
  6. state_bytes(values)·history_bytes(app, config)·weigh(topic, payload) 와 상태 열쇠 bulk 를 더해 체크포인트의 크기를 바이트로 재게 하세요.
  7. lost_demo(topic) 을 더해 장부를 새로 만들면 같은 thread_id 로도 아무것도 남지 않는 것을 재현하세요.
  8. /root/work/agckpt/ckpt_report.json/root/work/agckpt/ckpt_report.md 에 재어 본 값을 기록하세요.

참고

같은 스레드는 이어지고 다른 스레드는 따로 남는다

/root/work/agckpt/ckpt.pyANGLES·State·노드 셋(plan·write·reviewbuild_graph(checkpointer=None)·new_saver()·cfg(thread_id) 를 만드세요. 체크포인터를 준 그래프는 같은 thread_id 로 다시 돌리면 앞의 notes 위에 쌓이고, 다른 thread_id 는 빈 데서 시작해야 합니다.

graph.compile(checkpointer=checkpointer) 한 줄이 전부입니다. cfg(thread_id){"configurable": {"thread_id": thread_id}} 를 돌려주는 작은 함수로 두면 뒤 단계가 편합니다. notesoperator.add 를 붙여야 이어지는 것이 눈에 보입니다 — 붙이지 않으면 저장은 되는데 값이 덮어써져서 이어진 줄 모릅니다.

체크포인트는 단계마다 남는다

history(app, config)·history_len(app, config)·pending(app, config) 를 더하세요. history() 는 체크포인트를 오래된 것부터 늘어놓고, pending() 은 스냅샷마다 다음에 돌 노드 이름을(다 끝난 자리는 빈 문자열) 담은 목록을 돌려줍니다.

app.get_state_history(config) 는 제너레이터이고 최신 것부터 나옵니다. list() 로 받아 reversed() 하세요. 스냅샷의 next 는 튜플입니다 — 비어 있으면 더 돌 것이 없다는 뜻입니다. 세어 보면 노드 수보다 둘 많은데, 입력을 받은 자리와 다 끝난 자리가 하나씩 더 있기 때문입니다.

되감을 좌표를 찾는다

snapshot_before(app, config, node) 를 더하세요. 그 노드를 아직 돌지 않은 가장 최근 체크포인트를 돌려주고, 그런 자리가 없으면 None 을 돌려줍니다.

찾는 조건은 스냅샷의 next(node,) 인 것입니다. 오래된 것부터 늘어놓은 목록을 뒤에서부터 훑으면 '가장 최근' 을 먼저 만납니다. 돌려주는 것은 스냅샷 자체입니다 — 뒤 단계가 쓰는 것은 그 안의 config 이고, 거기에는 thread_id 말고 checkpoint_id 가 더 들어 있습니다.

그 자리에서 다시 돈다

replay(app, config, node) 를 더하세요. snapshot_before 로 좌표를 찾아 app.invoke(None, snapshot.config) 로 이어 돌리고, {"from": node, "result": 결과, "added": 늘어난 체크포인트 수} 를 돌려줍니다. 좌표가 없으면 ValueError 를 올리세요.

입력을 None 으로 주는 것이 핵심입니다. 상태를 주면 새로 시작하는 것이고, None 은 '저장된 그 시점부터 이어서 돌라' 는 뜻이 됩니다. added 는 되감기 전후로 history_len 을 재어 뺀 값입니다 — 다시 돈 노드 수만큼 늘어납니다.

값을 고쳐 다른 갈래로 간다

fork(app, config, node, values) 를 더하세요. 분기 전 스레드의 끝을 먼저 잡아 두고, app.update_state(snapshot.config, values) 로 갈래를 만든 뒤 그 config 로 이어 돌립니다. {"forked_config": ..., "result": ..., "original": 분기 전 끝의 값 딕셔너리, "total": 지금 체크포인트 수} 를 돌려주세요.

update_statecheckpoint_id 가 든 config 를 돌려줍니다. 그것을 버리고 원래 config 로 이어 돌리면 분기가 되지 않습니다. 그리고 분기한 뒤에는 thread_id 만으로 물어본 '지금' 이 새 갈래의 끝을 가리키므로, 원래 결과는 분기 전에 잡아 둔 스냅샷의 config 로 따로 읽어야 합니다.

크기로 잰다

상태 열쇠 bulkstate_bytes(values)·history_bytes(app, config)·weigh(topic, payload) 를 더하세요. state_bytesjson.dumps(values, ensure_ascii=False, sort_keys=True, default=str) 한 문자열의 UTF-8 길이입니다. weigh 는 같은 그래프를 bulk 없이 한 번, bulkpayload 를 담고 한 번 돌려 {"checkpoints": ..., "thin_total": ..., "fat_total": ..., "payload_bytes": ..., "carrying": ...} 를 돌려줍니다.

carrying 은 두 목록을 같은 자리끼리 견주어 크기 차이가 payload_bytes 이상인 체크포인트의 개수입니다 — 그 큰 값을 실제로 들고 있는 자리의 수라는 뜻입니다. 두 번 돌릴 때는 장부를 따로 쓰세요(new_saver() 를 두 번). 시간을 재지 마세요 — 기계와 부하에 따라 달라집니다. 바이트는 같은 상태면 늘 같습니다.

장부를 새로 만들면 사라진다

lost_demo(topic) 을 더하세요. 새 장부로 그래프를 만들어 run-1 스레드를 한 번 돌리고 체크포인트 수를 잰 뒤, 또 다른 새 장부로 그래프를 하나 더 만들어 같은 thread_id 로 물어봅니다. {"kept": ..., "after_restart": ..., "values_after_restart": ...} 를 돌려주세요.

MemorySaver 는 프로세스 안 메모리에 담습니다. 새 MemorySaver 를 만드는 것이 프로세스가 다시 뜬 것을 흉내 내는 가장 정직한 방법입니다. 새 장부에 물어보면 오류가 나지 않고 빈 값이 돌아옵니다 — 그래서 이 문제는 조용합니다. 실습과 시험에는 알맞지만 운영에는 맞지 않는 이유가 이것입니다.

재어 본 값으로 기록한다

/root/work/agckpt/ckpt_report.jsontopic·checkpoints_per_run·pending·base_score·fork_angle·fork_score·checkpoints_after_fork·payload_bytes·carrying·survives_restart 를, /root/work/agckpt/ckpt_report.md## 무엇이 저장되는가 ## 되감기와 분기는 어떻게 다른가 ## 크기로 잰 것 ## 사라지는 것 네 절로 쓰세요.

숫자는 손으로 적지 말고 여러분의 모듈을 실제로 돌려 얻은 값으로 채우세요. topic 은 여러분이 쓴 주제 문자열 그대로, fork_angle기본 이 아닌 갈래 이름입니다 — 채점기가 그 둘로 점수를 다시 계산해 대조합니다. 분기는 write 앞에서 치세요. payload_bytes 는 300 이상이어야 차이가 눈에 보입니다. survives_restart 는 참·거짓입니다.