มีการพูดคุยกันอย่างมากหลังจากการนำเสนอ เคอร์เนล Jalapeño MLA ที่งาน HotChips และความคิดเห็นติดตามจาก SemiAnalysis ในฐานะทีมฮาร์ดแวร์ของ OpenAI เราเพิ่งจะ แตะ ถึงอัญมณีเม็ดเล็กๆ นี้เพียงเล็กน้อยเท่านั้น: ความจริงที่ว่า AI กำลังเขียนเคอร์เนลของเรา และเมื่อมันทำเช่นนั้น เราไม่จำเป็นต้อง เข้าใจ ว่าเคอร์เนลทำงานอะไรแบบทีละบรรทัด เราจงใจละเว้นประเด็นสำคัญ: สิ่งนี้เกิดขึ้นได้อย่างไร? วิธีคิดที่ถูกต้องเกี่ยวกับเรื่องนี้ เมื่อเทียบกับวิธีการสร้างโค้ดแบบดั้งเดิมคืออะไร? เคอร์เนลที่ถูกปรับให้เหมาะสมนั้นมีความถูกต้องเทียบเท่ากับเคอร์เนลที่ไม่ได้ปรับให้เหมาะสมหรือไม่?
สำหรับภูมิหลังของผม ผมทำงานด้านคอมไพเลอร์สำหรับตัวเร่งความเร็ว (accelerators) มานานกว่าทศวรรษ ผมเป็นผู้เริ่มต้น XLA ซึ่งเป็นโครงสร้างพื้นฐานคอมไพเลอร์ที่ยอดเยี่ยมพร้อมทีมงานและความพยายามร่วมกันข้ามบริษัทที่โดดเด่น ในช่วง 2 ปีที่ผ่านมาที่ OpenAI ผมพยายามปรับแนวคิดใหม่ว่าคอมไพเลอร์ควรทำงานอย่างไรในยุคของ AI สูตรคอมไพเลอร์แบบใหม่จะดึงเอาจุดแข็งที่มีอยู่มาใช้ แต่ก็ปฏิเสธไม่ได้ว่ามีเครื่องมือทรงพลังใหม่ที่เราสามารถนำมาใช้ประโยชน์ในชุดเครื่องมือนี้ได้
นี่อาจเป็นการเดินทางที่ยาวนานพอสมควร แต่ผมหวังว่าจะช่วยให้เห็นภาพว่า AI ถูกนำมาใช้เพื่อทำให้ การปรับปรุงโปรแกรมคอมพิวเตอร์ เป็นอัตโนมัติได้อย่างไร นั่นคือ การคอมไพล์เพื่อเพิ่มประสิทธิภาพ (optimizing compilation) ผมคิดว่า ด้วย AI เราอาจได้สัมผัสกับสิ่งที่เราเรียกว่า "คอมไพเลอร์ 2.0" AI มีข้อจำกัดโดยพื้นฐานน้อยกว่าในสิ่งที่มันสามารถเสนอได้ และสิ่งที่มันเสนอนั้นเป็นผลมาจากการฝึกฝนและบริบทของโมเดล ซึ่งทำให้ผมจัดประเภทมันเป็น "ตัวปรับให้เหมาะสมแบบสุ่ม (stochastic optimizer)" – สิ่งนี้อาจก่อให้เกิดความท้าทาย แต่อย่างที่เราจะได้เห็น ก็เป็นที่มาของจุดแข็งที่ยิ่งใหญ่เช่นกัน...
งานวิจัยทางวิชาการและการประยุกต์ใช้ในอุตสาหกรรมจำนวนมากกำลังมุ่งไปในทิศทางนี้แล้ว และกำลังเปิดเผยศักยภาพของการมีส่วนร่วมของ AI ในขอบเขตของคอมไพเลอร์ที่ปรับให้เหมาะสมอย่างรวดเร็ว แต่เราอยู่ในจุดที่สมควรได้รับคำอธิบายในภาพรวมกว้างๆ
พื้นหลัง
คอมไพเลอร์รับโปรแกรมเข้ามาและส่งออกโปรแกรมที่ถูกแปลหรือปรับปรุงแล้ว
โปรแกรม ทั้งฝั่งอินพุตและเอาต์พุต มี ความหมายเชิงความหมาย (semantics) ที่บอกเราว่าโปรแกรมหมายถึงอะไร สามารถทำอะไรได้บ้าง และจะใช้เหตุผลเกี่ยวกับสิ่งเหล่านั้นที่มันสามารถทำได้อย่างไร
พวกเราที่ทำงานด้านคอมไพเลอร์มักมองว่ามันเหมือนฟังก์ชันบริสุทธิ์ – พวกมันรับโครงสร้างข้อมูลเข้ามาและส่งออกโครงสร้างข้อมูลที่ควรจะมีความหมายเชิงความหมายที่สอดคล้องกัน
บางครั้ง คอมไพเลอร์ของเราเน้นที่ "การลดระดับ (lowering)" หรือ "การแปล (translating)" ตัวอย่างเช่น พวกมันอาจรับภาษา C เข้ามาและส่งออกแอสเซมบลี x86-64 ซึ่งเรามักจะพิจารณาว่าเป็น "ระดับที่ต่ำกว่า" แต่บ่อยครั้งที่พวกมันทำมากกว่าแค่การแปลซึ่งเป็นส่วนย่อยของกระบวนการนั้น...
คอมไพเลอร์ของเรา ในทาง ปฏิบัติ เน้นที่ "การปรับให้เหมาะสม (optimizing)" พวกมันอาจรับโครงสร้างข้อมูลที่แทนโปรแกรม – ในภาษาของเราเรียกว่า "ตัวแทนกลาง (Intermediate Representation - IR)" – และพยายามสร้างเวอร์ชันที่ดีกว่าของโปรแกรมนั้น บางครั้ง "ดีกว่า" หมายถึงใช้รอบการทำงานน้อยลงในการรัน บางครั้งหมายถึงจะมีโค้ดที่ไม่จำเป็นน้อยลง บางครั้งหมายถึงการเฉพาะทางสำหรับสิ่งที่เราสามารถพิสูจน์ได้ว่า "ต้องเป็นจริง" เกี่ยวกับโปรแกรม (การประเมินบางส่วน - partial evaluation)
ทีนี้ ลองพิจารณาสั้นๆ ว่า LLM ถูกสร้างขึ้นมาเพื่อแปลข้อความมนุษย์จากภาษาหนึ่งไปอีกภาษาหนึ่ง เห็นได้ชัดว่าการแปลอยู่ในขอบเขตความสามารถของพวกมัน และเราสามารถเห็นผ่านการใช้ LLM ในงานประจำวันของเราว่าพวกมันยังสามารถเขียนวิธีแก้ปัญหาใหม่และปรับปรุงวิธีแก้ปัญหาที่มีอยู่ได้อีกด้วย พวกเราที่เป็นนักเขียนโค้ดหลายคนก็มีประสบการณ์ในการขอให้ LLM "ปรับโค้ดส่วนนี้ให้เหมาะสม" และพวกมันก็ทำได้อย่างน่าทึ่ง (อย่างไรก็ตาม เราจำเป็นต้องรู้ว่าพวกมันปรับโค้ดได้ถูกต้องหรือไม่ ซึ่งเราจะพูดถึงต่อไป!) นี่เป็นเพียงเพื่อเน้นว่า LLM มีความสามารถที่เรามองหาในคอมไพเลอร์ที่ปรับให้เหมาะสม
การปรับให้เหมาะสมและความเหมาะสมที่สุด
คอมไพเลอร์ที่ปรับให้เหมาะสมนั้น ไม่น่าแปลกใจเลย ที่พยายาม เพิ่มความเหมาะสมที่สุด (optimality) ของโปรแกรมที่พวกมันกำลังทำงานอยู่ โดยมีวัตถุประสงค์บางอย่าง (โดยปกติคือเวลาในการทำงาน) นั่นเป็นสิ่งที่ ยากมาก ที่จะทำในกรณีทั่วไป สำหรับโปรแกรมตามอำเภอใจ จนมีทฤษฎีบทที่เรียกว่า ทฤษฎีบทการจ้างงานเต็มรูปแบบสำหรับวิศวกรคอมไพเลอร์ (ผมเพิ่งรู้เรื่องนี้หลังจากเลือกเป็นวิศวกรคอมไพเลอร์ แต่มันก็ยังทำให้ผมสบายใจขึ้น!)
"ซูเปอร์ออปติไมเซอร์ (Superoptimizers)" เป็นสาขาย่อยที่น่าทึ่งของคอมไพเลอร์ที่ปรับให้เหมาะสม ลองนึกภาพว่ามีโปรแกรมที่กำหนดให้ และเราสามารถบอกได้ว่ามันทำอะไรผ่านความหมายเชิงความหมาย อะไรคือโปรแกรมที่ เหมาะสมที่สุด ที่มีความหมายเชิงความหมายเดียวกัน? นั่นคือสิ่งที่ซูเปอร์ออปติไมเซอร์พยายามจัดการ และมันเป็นปัญหาการค้นหาโดยพื้นฐาน...
ลองนึกภาพว่าผมพยายามหาโปรแกรมที่สั้นที่สุดที่มีความหมายเชิงความหมายเดียวกัน และผมมีวิธีถามว่าโปรแกรมตัวเลือกนั้นมีความหมายเชิงความหมายเดียวกันหรือไม่ ตามสมมุติฐานแล้ว ผมสามารถแจกแจงทุกโปรแกรมตามลำดับวัตถุประสงค์ และเลือกโปรแกรมที่เล็กที่สุดที่มีความหมายเชิงความหมายเดียวกัน
อย่างไรก็ตาม การแจกแจงทุกโปรแกรมตามลำดับวัตถุประสงค์ฟังดูไม่สามารถจัดการได้ หนึ่งในเอกสารวิชาการที่ผมชอบที่สุด ซึ่งตีพิมพ์ในปี 2013 ชื่อ "STOKE" (Stochastic Superoptimization) ได้ตั้งคำถามว่า "แล้วถ้าเราแค่ ปรับแต่ง โปรแกรมแบบสุ่มซ้ำแล้วซ้ำเล่า ในที่สุดเราจะสังเกตเห็นโปรแกรมที่ดีที่สุดหรือไม่?" พวกเขาเสนอว่าผ่านการเดินแบบสุ่ม (random walk) (และกับเพื่อน机器学习ดั้งเดิมของเรา Markov Chain Monte Carlo / Metropolis-Hastings) ในที่สุดคุณจะเห็นโปรแกรมที่เหมาะสมที่สุดนั้น
การปรับแต่งแบบ Monte Carlo โดยทั่วไปนั้นโง่ (คุณสุ่มเลือกการปรับแต่ง) แต่ก็เร็ว LLM นั้นฉลาดมาก (มีโทเค็นการให้เหตุผลจำนวนมาก) แต่ค่อนข้างช้าเมื่อเทียบกัน
จะเป็นอย่างไร ถ้าแทนที่จะใช้การปรับแต่งแบบ Monte Carlo ที่โง่/เร็ว เรามี LLM คิดหาทิศทางที่จะพาโปรแกรมไป? เราจะมีตัวปรับให้เหมาะสมแบบสุ่มที่ ฉลาดมาก ค่อยๆ พาโปรแกรมของเราผ่านพื้นที่โปรแกรมที่ถูกปรับให้เหมาะสม
สัญชาตญาณสำหรับการปรับให้เหมาะสม
ลองถอยกลับมาหน่อย คิดถึงคนที่คุณรู้จักซึ่งเป็นตัวแทนของ "การปรับแต่งโค้ดให้เหมาะสมที่สุด" ได้ดีที่สุด เรียกสั้นๆ ว่า "ออลลี่นักปรับให้เหมาะสม (optimizin' Ollie)" ออลลี่อาจมีสัญชาตญาณโดยธรรมชาติว่าการปรับแต่งโค้ดแบบใดที่อาจให้ผลลัพธ์ ออลลี่อาจลองทำบางอย่างเพื่อดูว่ามันใช้ได้หรือไม่ และถ้ามันไม่ได้ผล ก็ย้อนกลับและลองอย่างอื่น แต่พวกเขามีสัญชาตญาณบางอย่างว่าสิ่งใดที่เป็นไปได้ และพวกเขาจะ อาจ เอาชนะคอมไพเลอร์ได้อย่างไร
สัญชาตญาณที่ออลลี่มีนั้นมักจะเกินกว่าที่คอมไพเลอร์ทำได้ แม้ว่าคอมไพเลอร์ที่ปรับให้เหมาะสมสมัยใหม่จะให้ผลลัพธ์ที่น่าประทับใจ แต่พวกมันก็ใช้กฎและฮิวริสติกที่ค่อนข้างง่าย ในศัพท์เทคนิค พวกมันใช้แนวคิดของการแปลงการไหลของข้อมูลเฉพาะที่ (local dataflow transform) ที่รันจนถึงจุดคงที่ (fixed point) นอกจากนี้เรายังจัดลำดับเฟสของการพิจารณา กล่าวคือ เราสร้างไปป์ไลน์คอมไพเลอร์เพื่อพิจารณา A แล้วจึง B แต่ไม่ใช่ปัญหารวม AB ตัวจัดตารางเวลา (schedulers) และตัวจัดสรรทะเบียน (register allocators) เป็นตัวอย่างที่ฉาวโฉ่ของเรื่องนี้ ปริญญาเอกหลายคนพยายามแก้ปัญหาตัวจัดตารางเวลา-ตัวจัดสรรทะเบียนแบบรวม (เพื่อให้ได้ประโยชน์จากการยุบลำดับเฟส) แต่มันก็เป็นเรื่องท้าทายที่จะทำให้ใช้งานได้จริง
นี่คือเหตุผลที่ความเชี่ยวชาญของออลลี่มีค่า ออลลี่มักจะรู้วิธีสร้างสมดุลระหว่างปัญหา NP-complete หลายปัญหาด้วยฮิวริสติกที่ออกแบบเฉพาะสำหรับสถานการณ์ ดังนั้นจึงมีความตระหนักรู้และความไวต่อบริบทที่เฉพาะเจาะจงมากขึ้น ออลลี่ยังสามารถใช้เทคนิคที่คอมไพเลอร์ที่ปรับให้เหมาะสมอาจไม่สามารถนำไปใช้ได้อย่างมีกำไร โดยเฉพาะอย่างยิ่งเมื่อใช้ร่วมกัน เช่น การแยกส่วน (outlining) หรือการสร้าง ABI หรือการแปลงที่กำหนดเองเพื่อเปิดใช้งานการทำเวกเตอร์ (vectorization) หรือสิ่งอื่นๆ อีกมากมายที่ทำให้เราบ่นว่า "ฉันหวังว่าคอมไพเลอร์จะมีวิธีทำแบบนี้..."
ทีนี้ ลองพิจารณาว่า AI ผ่านสิ่งอำนวยความสะดวกในการให้เหตุผลใดๆ ที่มันมี อาจสามารถทำหน้าที่เป็นออลลี่ตัวจิ๋วได้ มันอาจไม่มีสัญชาตญาณที่ตรงกันในแง่ของสิ่งที่จะเกิดผล แต่มันมีแนวคิดคร่าวๆ ว่าสิ่งใดจะทำกำไรได้ และมันสามารถยิงประตูได้หลาย หลาย ครั้ง
ด้วยวิธีการนี้ ไม่เหมือนในเอกสาร STOKE เราไม่สามารถ รับประกัน ได้ว่าเมื่อเวลาผ่านไปจนถึงอนันต์ เราจะเห็นโปรแกรมที่ เหมาะสมที่สุด แต่เนื่องจาก AI มีสิ่งอำนวยความสะดวกในการให้เหตุผลที่ "เหมือนมนุษย์มากกว่า" มันจึงสามารถได้รับแรงผลักดันแบบมนุษย์อย่างมีนัยสำคัญต่อหน่วยเวลา
เชื่อมโยงกลับมา: เคอร์เนล MLA
ให้ผมเริ่มโดยบอกว่า: ผม ไม่ รู้ว่าโค้ดระดับต่ำอะไรที่ AI ถ่มออกมาสำหรับเคอร์เนล Jalapeño MLA ของเรา แต่ผม รู้ วิธีพิมพ์ numpy สำหรับ MLA
ในคอมไพเลอร์ XLA ที่ผมเคยทำงาน เราจะรวมการดำเนินการ numpy เหล่านั้นเข้าด้วยกันเป็นกลุ่มๆ แล้วใช้เมตาโปรแกรมที่เรียกว่า "emitter" เพื่อลดระดับลงเป็นลูป คำสั่ง และพื้นฐานระดับล่าง

เมื่อคอมไพเลอร์ / โปรแกรม emitter ของ XLA ทำเช่นนั้น ผมไม่จำเป็นต้องสนใจว่าแอสเซมบลีอะไรออกมาทางด้านหลัง สำหรับตัวปรับให้เหมาะสมแบบสุ่มของเรา AI ในทางแนวคิดจะเข้ามาแทนที่เมตาโปรแกรม emitter – มันทั้งลดระดับและปรับให้เหมาะสม และเราสามารถขอให้มันปรับให้เหมาะสมต่อไปเรื่อยๆ จนถึงขีดจำกัดสูงสุด (roofline)

ผมหวังว่าสิ่งนี้จะทำให้ชัดเจนว่า AI เข้ามาอยู่ตรงไหน และมันคล้ายคลึงกับส่วนประกอบในระบบคอมไพเลอร์ที่ปรับให้เหมาะสมที่มีอยู่อย่างไร นอกจากนี้ยังเป็นประโยชน์ที่จะคิดว่า: ชั้นที่เราถือว่าเป็น "รหัสแอสเซมบลี" นั้นกำลัง เลื่อนขึ้นไป เมื่อคุณพิมพ์ C++ ปกติและคอมไพล์ที่ -O3 (ระดับการปรับให้เหมาะสมทั่วไปสูงสุด) คุณไม่ได้คาดหวังว่าจะเข้าใจแอสเซมบลีที่ออกมา แม้ว่าคุณจะเข้าใจ C++ ที่คุณพิมพ์เข้าไปก็ตาม เรากำลังทำสิ่งที่คล้ายคลึงกันที่นี่ แต่มีข้อกำหนดอินพุตทางคณิตศาสตร์ที่สูงกว่าและเป็นนามธรรมมากกว่า
ทีนี้ คำถามสำคัญคือ เราจะ ตรวจสอบ อย่างไรว่าโปรแกรมที่เราได้รับจาก AI นั้นเทียบเท่ากับคำอธิบายระดับสูง / numpy จริงๆ กลไกการตรวจสอบนั้นสร้างความถูกต้อง (soundness) ของกระบวนการปรับให้เหมาะสมแบบสุ่มของ AI ผมคาดว่าโพสต์บล็อกในอนาคตอาจลงรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้ แต่สำหรับตอนนี้ พูดสั้นๆ ก็คือ การทดสอบความเท่าเทียมทางความหมาย (semantic equivalence) เป็นไปได้ และเราทำมัน โปรแกรมสำหรับตัวเร่งความเร็วนั้นเอื้ออำนวยเป็นพิเศษต่อสัญญาที่แข็งแกร่งและสมบูรณ์ ซึ่งเราสามารถตรวจสอบได้ว่า "ตรงกับสิ่งที่โปรแกรมที่ AI ปรับให้เหมาะสมทำจริงๆ" เนื่องจากพวกมันค่อนข้างเป็นคณิตศาสตร์และเน้นการไหลของข้อมูลในบริบทกว้างๆ
โปรดทราบว่าเทคนิคที่เกี่ยวข้องมากมายในพื้นที่นี้ได้รับการบุกเบิกโดยความพยายามในสาขาย่อยของการสังเคราะห์โปรแกรม (program synthesis) ในขณะที่คอมไพเลอร์ที่ปรับให้เหมาะสมพูดว่า "นี่คือโปรแกรมที่มีความหมายเชิงความหมาย ทำให้มันดีขึ้น แต่มีความหมายเชิงความหมายที่เทียบเท่ากัน!" การสังเคราะห์โปรแกรมพูดว่า "ฉันเชื่อว่ามีโปรแกรมที่มีความหมายเชิงความหมายเหล่านี้ โปรดพยายามหาสิ่งที่ดีที่สุดเท่าที่คุณจะทำได้" การสังเคราะห์โปรแกรมเป็นปัญหาที่ยากกว่าการคอมไพล์เพื่อเพิ่มประสิทธิภาพ แต่มันก็มีข้อจำกัดโดยพื้นฐานน้อยกว่า มันคือสิ่งที่มนุษย์อย่างออลลี่ทำเมื่อพวกเขาทำได้ดีกว่าคอมไพเลอร์ที่ปรับให้เหมาะสม และมันเป็นสิ่งที่ AI สามารถช่วยเราทำให้เป็นอัตโนมัติได้ในตอนนี้ AI สามารถดึง "แรงบันดาลใจ" จากโปรแกรมดั้งเดิม แต่มันไม่จำเป็นต้องทำการแปลงเฉพาะที่เล็กน้อยกับมันเท่านั้น คอมไพเลอร์ที่ปรับให้เหมาะสมแบบคลาสสิก จะไม่ เห็น "โอ้ คุณเขียน bubble sort" และโดยการเข้าใจสัญญา ก็เปลี่ยนมันเป็น quick-sort แต่ทั้งออลลี่และ AI สามารถทำเช่นนั้นได้ นี่คือสิ่งที่ทำให้เราอยู่ในระบอบการสังเคราะห์โปรแกรมด้วยการปรับให้เหมาะสมแบบสุ่ม มากกว่าระบอบคอมไพเลอร์ที่ปรับให้เหมาะสมแบบคลาสสิก
ทั้งหมดนี้มารวมกันในข้อเท็จจริงที่ว่าคุณสามารถเริ่มต้นด้วยสิ่งที่ "ไม่ไกลจาก numpy มากนัก" รอ 48 ชั่วโมง และมีเคอร์เนลที่ปรับให้เหมาะสมซึ่งมีความหมายเชิงความหมายเดียวกัน ดังที่เราแสดงในการบรรยาย HotChips ของเรา:

ดังที่สไลด์ยังระบุไว้ด้วย บนเครื่องของเรา เรามักจะสังเกตเห็นว่า AI สามารถไต่อันดับประสิทธิภาพเหนือกว่าผู้เชี่ยวชาญมนุษย์ของเรา แม้แต่ ในเคอร์เนลที่เรารู้สึกว่าปรับแต่งมาค่อนข้างดีแล้ว บ่อยครั้งที่ยังมีเปอร์เซ็นต์ที่ทำได้ดีพอสมควรเหลืออยู่ เพียงเพราะการผสมผสาน/การเรียงสับเปลี่ยนที่หลากหลายซึ่งอาจจำเป็นต้องสำรวจ สิ่งเหล่านี้มักจะน่าเบื่อหน่ายอย่างเหลือทนสำหรับวิศวกรประสิทธิภาพที่เป็นมนุษย์
สรุปและบทสรุป
คอมไพเลอร์ ในท้ายที่สุดแล้ว ก็คือฟังก์ชันหนึ่ง เราส่งโปรแกรมของเราให้ฟังก์ชันนั้น และเราได้รับเวอร์ชันที่ดีกว่าของโปรแกรมของเรากลับมา โปรแกรมที่เราได้รับออกมาและโปรแกรมที่เราส่งเข้าไป เราคาดหวังให้มีความหมายเชิงความหมายเดียวกัน
คอมไพเลอร์ที่ปรับให้เหมาะสมแบบดั้งเดิมทำให้โปรแกรมดีขึ้นผ่านกฎการไหลของข้อมูลและฮิวริสติก สิ่งเหล่านี้สามารถเข้าใจที่มาได้อย่างสมบูรณ์ แต่อาจมีข้อจำกัดมากกว่าในการดำเนินการที่สามารถทำได้
ในทางตรงกันข้าม AI ในฐานะตัวปรับให้เหมาะสมแบบ สุ่ม เพียงแค่ต้อง "คิดหนัก" และถ่มบางสิ่งออกมา การดำเนินการของมันไม่ได้ถูกจำกัดโดยพื้นฐาน ทำให้มันคล้ายคลึงกับผู้เชี่ยวชาญมนุษย์ของเรามากกว่า เราจำเป็นต้องมีวิธีตรวจสอบว่าโปรแกรมที่มันถ่มออกมานั้นถูกต้องและใช้ความหมายเชิงความหมายเดียวกันกับที่เราใส่เข้าไป และเรามีสิ่งเหล่านั้นอยู่แล้ว และการปรับให้เหมาะสมด้วย AI แบบนี้เหมาะเป็นพิเศษกับการดำเนินการทางคณิตศาสตร์ซึ่งมีสัญญาที่แข็งแกร่งมาก สัญญาเหล่านี้ช่วยหลีกเลี่ยงความจำเป็นในการทำความเข้าใจว่าเคอร์เนลทำงานอะไรแบบทีละบรรทัด
นั่นคือวิธีที่เราได้เคอร์เนล MLA ที่สร้างโดย AI!





