1 คะแนน โดย GN⁺ 2024-03-12 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • CPython PR #116338 ได้ merge การเปลี่ยนแปลงที่ทำให้สามารถปิดใช้งาน GIL ได้ใน free-threaded build ด้วย PYTHON_GIL=0 หรือ -X gil=0 เข้าไปยัง python:main
  • เพื่อเปิดทางให้สามารถเปิด GIL กลับมาได้อีกใน runtime จึงมีการ initialize โครงสร้างข้อมูลที่เกี่ยวข้องกับ GIL ตามปกติเหมือนเดิม และจัดการการปิดใช้งานด้วยการตั้งค่าแฟลกตอนเริ่มต้นเพื่อให้ take_gil() และ drop_gil() return ออกก่อน
  • จากการตรวจสอบเบื้องต้น เมื่อตั้งค่า PYTHON_GIL=0 พบว่าบางเทสต์และโปรแกรมขนาดเล็กที่ไม่ใช้เธรดทำงานได้ตามปกติ และโปรแกรมเธรดที่พื้นฐานมาก ๆ ก็ทำงานได้เป็นบางครั้ง แต่ test suite ทั้งชุด crash อย่างรวดเร็วที่ test_asyncio
  • ระหว่างการรีวิว ได้มีการเพิ่มเทสต์ของ PYTHON_GIL, เอกสารประกอบ, ตัวเลือก -X gil และการสะท้อนค่าใน sys.flags รวมถึงแก้การจัดการการตั้งค่าให้ PYTHON_GIL=1 บังคับ การเปิดใช้งาน GIL
  • งานต่อเนื่องถูกแยกออกเป็นประเด็นการเปิด GIL กลับเมื่อโหลดส่วนขยายที่ไม่เข้ากัน และประเด็นการปิด GIL เป็นค่าเริ่มต้น โดยการเปลี่ยนแปลงนี้เพิ่มพื้นผิวการควบคุม GIL ใน free-threaded build ของ Python 3.13

การเปลี่ยนแปลงที่ merge แล้ว

  • CPython PR #116338 ครอบคลุมการเปลี่ยนแปลง gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0
  • colesbury เป็นผู้ merge เข้า python:main เมื่อวันที่ 11 มีนาคม 2024
  • ขนาดของการเปลี่ยนแปลงแสดงเป็น 12 ไฟล์, เพิ่ม 163 บรรทัด, ลบ 1 บรรทัด
  • ฟีเจอร์เป้าหมายคือออปชันขณะรันเพื่อปิดใช้งาน GIL ใน free-threaded build ไม่ใช่ใน build ปกติ

วิธีปิดใช้งาน GIL

  • ใน free-threaded build สามารถปิดใช้งาน GIL ได้ด้วยการตั้งค่าต่อไปนี้
    • PYTHON_GIL=0
    • -X gil=0
  • เพื่อให้สามารถเปิด GIL กลับมาได้ใน runtime โครงสร้างข้อมูลที่เกี่ยวข้องกับ GIL ทั้งหมดจะถูก initialize ตามปกติ
  • การปิดใช้งานจริงใช้วิธีตั้งค่าแฟลกตอนเริ่มต้น
    • แฟลกนี้ทำให้ take_gil() และ drop_gil() return ออกก่อน
  • ระหว่างการรีวิว ยังมี commit เพิ่มเติมเพื่อกำหนด enable_gil ให้ถูกต้องเมื่อ PYTHON_GIL=1

การทดสอบและข้อจำกัดในปัจจุบัน

  • มีการตรวจสอบบางเทสต์และโปรแกรมขนาดเล็กด้วยการตั้งค่า PYTHON_GIL=0
    • เทสต์และโปรแกรมขนาดเล็กที่ไม่ใช้เธรดได้รับการยืนยันว่าทำงานได้ตามปกติ
    • โปรแกรมเธรดที่พื้นฐานมาก ๆ ทำงานได้เป็นบางครั้ง
  • test suite ทั้งชุด crash อย่างรวดเร็ว และบันทึกตำแหน่งไว้ที่ test_asyncio
  • มีการจอง builder test ที่เกี่ยวข้องกับ NoGIL หลายครั้งด้วยคำสั่ง !buildbot nogil
    • x86-64 MacOS Intel ASAN NoGIL PR
    • x86-64 MacOS Intel NoGIL PR
    • ARM64 MacOS M1 Refleaks NoGIL PR
    • ARM64 MacOS M1 NoGIL PR
    • AMD64 Ubuntu NoGIL Refleaks PR
    • AMD64 Ubuntu NoGIL PR
    • AMD64 Windows Server 2022 NoGIL PR

ขอบเขตที่เพิ่มเข้ามาระหว่างการรีวิว

  • corona10 เสนอว่าน่าจะคุ้มค่าที่จะเพิ่มการทดสอบ environment variable ใน Lib/test/test_cmd_line.py
  • หลังจากนั้นมี commit ต่อไปนี้ถูกเพิ่มเข้ามา
    • Add test for PYTHON_GIL in test_cmd_line
    • Set enable_gil properly when PYTHON_GIL=1
    • Don't add 'enable_gil' to test_embed in normal builds
  • colesbury มองว่าควรจัดทำเอกสารพร้อมกับช่วงที่เพิ่ม environment variable
    • โดยอ้างอิงว่ามีการจัดทำเอกสารสำหรับ configure flag --disable-gil อยู่แล้ว
    • เนื้อหาเอกสารควรระบุว่าสามารถใช้ได้เฉพาะใน free-threaded build, 0 คือบังคับปิด GIL, 1 คือบังคับเปิด GIL และเป็นสิ่งใหม่ใน Python 3.13
  • หลังจากนั้นได้มี commit Document PYTHON_GIL environment variable เพิ่มเข้ามา

การเพิ่มตัวเลือก -X gil และการ merge ขั้นสุดท้าย

  • หลังจากการพูดคุยบน Discord จึงตัดสินใจเพิ่ม ตัวเลือก -X ที่ใช้ร่วมกับ environment variable ด้วย
  • ชื่อ PR ถูกเปลี่ยนจากรูปแบบที่กล่าวถึงแค่ PYTHON_GIL=0 ให้ครอบคลุมถึง PYTHON_GIL=0 or -X gil=0 ด้วย
  • commit ที่เพิ่มเข้ามามีเนื้อหาต่อไปนี้
    • Add -X gil option, add to sys.flags, modify test to cover env var… and option
    • Fix link to -X gil
    • Fix PYTHON_GIL versionchanged line
    • Clarify test_flags in normal builds
  • ericsnowcurrently, erlend-aasland, corona10, colesbury อนุมัติการเปลี่ยนแปลงนี้
  • merge commit คือ 2731913 และหลัง merge แล้ว vstinner ตอบสนองต่อการเปลี่ยนแปลงนี้ว่า “น่าสนใจและน่ากลัวมาก”

งานต่อเนื่อง

  • มีการแยกงานต่อเนื่องออกเป็น 2 ประเด็น
    • #116322: งานเปิดใช้งาน GIL กลับอีกครั้งเมื่อโหลดส่วนขยายที่ไม่เข้ากัน
    • #116329: งานทำให้ปิด GIL เป็นค่าเริ่มต้น
  • PR ปัจจุบันนี้ไม่ใช่การเปลี่ยนค่าเริ่มต้นของ GIL แต่เป็นการเปลี่ยนแปลงที่ทำให้ผู้ใช้ใน free-threaded build สามารถควบคุมสถานะ GIL ได้ผ่าน environment variable หรือออปชัน -X

1 ความคิดเห็น

 
GN⁺ 2024-03-12
ความคิดเห็นบน Hacker News
  • ฝากลิงก์เพิ่มเติมไว้สำหรับคนที่สนใจงาน no-GIL: [0], [1]
    [0] Multithreaded Python without the GIL
    https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
    [1] Github repo
    https://github.com/colesbury/nogil

  • น่าตื่นเต้นว่าจะทำให้ Python พื้นฐานเร็วขึ้นได้อีกแค่ไหน คุณค่าหลักของ Python เองก็กำลังถูกท้าทาย เพราะมีเครื่องมือออกมาช่วยบรรเทาปัญหานี้มากเกินไป
    เครื่องมือเพิ่มความเร็วที่นึกออกคือ Mojo, pytorch, triton, numba, taichi มีความพยายามแก้ปัญหานี้เยอะมาก จนครั้งก่อนที่ลองจะใช้สักตัว รู้สึกท่วมท้นเพราะตัวเลือกเยอะเกินไป สุดท้ายเลือก taichi ซึ่งค่อนข้างสนุกและใช้ง่าย แต่ขอบเขตการใช้งานค่อนข้างจำกัด

    • Mojo ควรถูกมองว่าเป็นการโจมตีต่อ ecosystem ของ Python เพราะมันเป็น superset ของ Python ถึงจะใช้ Python ได้ แต่มันไม่ใช่ Python เอง
      Taichi ถูกประเมินต่ำเกินไปจริง ๆ มันทำงานได้บนทุกแพลตฟอร์มรวมถึง Metal มีตัวอย่างเยอะ และเขียนโค้ดง่าย ที่สำคัญที่สุดคือมันผสานเข้ากับ ecosystem และไม่ได้มาแทนที่ ecosystem เดิม
      https://github.com/taichi-dev
      วิดีโอเดโมดี ๆ ที่แสดงให้เห็นว่าใช้ Taichi ทำอะไรได้บ้าง: https://www.youtube.com/watch?v=oXRJoQGCYFg
      https://www.youtube.com/watch?v=WNh4Q7-OSJs
      https://www.taichi-lang.org/
  • สงสัยว่าทำไมวิธี biased reference counting ที่อธิบายใน https://peps.python.org/pep-0703/ ถึงให้ความเป็นมิตรกับเธรดเดียวเท่านั้น และถ้าเข้าถึงจากเธรดอื่นก็ต้องเพิ่ม/ลดแบบ atomic
    ใน implementation อื่น ๆ เช่น Rust crates หลายตัวที่ทำ biased reference counting เคยเห็นวิธีที่เพิ่มแบบ atomic เฉพาะตอนย้ายไปเธรดใหม่ จากนั้นเธรดนั้นจะเพิ่ม/ลดแบบ non-atomic ไปจนกว่าจะกลับถึง 0 แล้วค่อยลดแบบ atomic ครั้งสุดท้าย สงสัยว่าเป็นเพราะมันเป็นการต่อเติมเข้ากับระบบเดิม จึงมี PyObject เดี่ยวอยู่แล้ว และไม่สามารถสลับไปชี้ object แบบ thread-local ตัวใหม่ได้หรือเปล่า

    • ในอนาคต CPython อาจ implement การโอน ownership ได้ แต่จะซับซ้อนขึ้นอีกหน่อย
      ใน Rust การ "move" เพื่อโอน ownership เป็นส่วนหนึ่งของภาษา แต่ใน C หรือ Python ไม่มีแนวคิดที่เทียบเท่ากัน จึงยากที่จะตัดสินว่าเมื่อไรควรโอน ownership และเธรดไหนควรเป็นเจ้าของใหม่ ใช้ heuristic ได้ เช่น อาจสละหรือโอน ownership ตอนใส่ object ลงใน queue.SimpleQueue แต่ถึงอย่างนั้นก็ยังยากที่จะรู้ล่วงหน้าว่าเธรดไหนจะ "get" object ที่อยู่ในคิว
      ประโยชน์ด้าน performance ก็น่าจะน้อยด้วย object จำนวนมากถูกเข้าถึงแค่จากเธรดเดียว บาง object ถูกหลายเธรดเข้าถึง แต่ object ที่ถูกเข้าถึงแบบ exclusive จากเธรดเดียว แล้วภายหลังถูกเข้าถึงแบบ exclusive จากอีกเธรดหนึ่งนั้นพบได้น้อย
  • เพิ่งอ่านข่าว tranched bread ไปก่อนหน้านี้ แล้วตอนนี้ยังมีอันนี้อีกเหรอ? เป็นยุคที่น่าทึ่งจริง ๆ
    ตอนโปรเจกต์ Unladen Swallow [1] ค่อย ๆ เงียบหายไปก็รู้สึกเสียดายนิดหน่อย ดีใจที่เห็น Python กลับมาอยู่บนเส้นทาง optimization หลักอีกครั้ง
    [1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow

  • อยากให้ช่วยอธิบายเหมือนอธิบายให้เด็กห้าขวบฟัง
    เข้าใจในเชิงแนวคิดว่า GIL คืออะไร แต่ผลกระทบของการเปลี่ยนแปลงนี้คืออะไร? ตอนนี้ package ต่าง ๆ จะพัง โดยแลกกับการคาดหวังว่า performance โดยรวมจะดีขึ้นหรือเปล่า?

    • เมื่อก่อนเพราะ GIL ทำให้แทบไม่ได้เขียน Python แบบหลายเธรดจริง ๆ เธรดมักใช้เพื่อจัดการงานหลายอย่างที่อาจค้างอยู่กับ I/O อิสระ ซึ่งแน่นอนว่าพบได้บ่อยและมีประโยชน์ แต่ไม่ได้ช่วย performance ของโค้ด Python ที่เน้น CPU
      แม้ไม่ใช่งาน CPU หนัก ๆ การเปลี่ยนแปลงนี้ก็ยังมีประโยชน์ได้ ช่วงหลังโค้ดจำนวนมากเขียนด้วยฟีเจอร์ภาษา asyncio แบบ native ของ Python มันทำงานในเธรดเดียวโดย yield การรันผ่าน async/await เหมือน NodeJS และใช้แค่เธรดเดียวก็ทำ throughput ได้ดีระดับหลายพัน requests ต่อวินาที
      แต่ปัญหาใหญ่คือทันทีที่ทำ งาน CPU ใด ๆ มันจะบล็อก coroutine อื่นทั้งหมด ทำให้เกิดปัญหาคลุมเครือสารพัดและทำให้จำนวน requests ต่อวินาทีพัง เช่น คุณอาจเห็น I/O timeout แบบสุ่มใน coroutine หนึ่ง ทั้งที่สาเหตุจริงคือ coroutine อีกตัวหนึ่งใช้ CPU อยู่ชั่วครู่ การสังเกตให้รู้ว่าทำไมเรื่องนี้เกิดขึ้นก็ยากมาก asyncio มีฟังก์ชัน asyncio.to_thread() [1] ที่ช่วยย้ายงานที่บล็อกออกจากเธรดหลัก แต่เพราะ GIL มันจึงไม่ได้แยกงานที่เน้น CPU ออกจริง ๆ เพื่อไม่ให้ไปรบกวน coroutine อื่น
      [1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
    • ถ้า package ใดพึ่งพา GIL ก็จะเปิดใช้ GIL package จะไม่พัง
  • สำหรับคนที่สงสัย GIL ย่อมาจาก Global Interpreter Lock

  • มีแหล่งข้อมูลที่สรุปภาพใหญ่กว่านี้ได้ดีไหม?

  • ในที่สุดก็รอดู benchmark ของเครื่องมือต่าง ๆ ได้แล้ว