ก้าวแรก HPC #10: ทลายขีดจำกัดด้วย ZeroMQ

เกริ่นนำ

ที่ผ่านมาเราได้เรียนรู้ในการรีดกำลังของ CPU มาใช้ให้เต็มประสิทธิภาพซึ่งก็ผ่านไปด้วยดี ผลลัพธ์เป็นที่น่าพอใจ ในบทนี้เราจะไปกันต่อ เมื่อเราสูบทรัพยากรของเครื่องคอมพิวเตอร์หมดแล้ว เราก็ต้องไปสูบเครื่องอื่นต่อไป ใช่ครับ เรากำลังพูดถึงการทำงานแบบคลัสเตอร์ นั่นก็ไม่ใช่เรื่องใหม่สำหรับเรานัก เพราะได้ผ่านหูผ่านตามาจาก Apache Spark ไปแล้ว ตัวของ Apache Spark นั้นให้เราได้ทุกสิ่ง เหลือเพียงอย่างเดียวที่เรายังไม่ได้ก็คือประสิทธิภาพ ในบทนี้เรามาทวงถามหาประสิทธิภาพกัน

เปิดเรื่องด้วย Message Queue

บทความนี้เราจะใช้งาน ZeroMQ ซึ่ง MQ ในที่นี้ย่อมาจากคำว่า message queue เราจึงควรทำความเข้าใจเป็นปฐมกันก่อน คำว่า message queue นั้นเป็นการสื่อสารระหว่างโปรเซสแบบหนึ่ง (interprocess communication : IPC) ตัว IPC นั้นมีหลายแบบ ตั้งแต่แบบพื้นฐานที่เราเรียกว่า shared memory ซึ่งเป็นการสื่อสารที่เทร็ดหรือโปรเซสสองตัวขึ้นมาใช้หน่วยความจำร่วมกัน เราผ่านตาไปแล้วในบทก่อนๆ แล้ว ตอนนั้นเราใช้ list หรือ queue หรือตัวแปรอื่นๆ ร่วมกันหลายเทร็ด ข้อดีนั้นก็คือเร็วมาก เพราะเป็นหน่วยความจำ RAM ที่เร็วมาก ข้อเสียก็คือข้ามเครื่องไม่ได้ เพราะเครื่องแต่ละเครื่องมีหน่วยความจำเป็นของตัวเอง

IPC ยังมีอีกหลายแบบนะครับ ผมยังไม่ขอแจงแต่ละแบบ เอาไว้ถ้ามีเหตุต้องใช้ ผมจะกลับมาอธิบายให้ฟังกันอีกที ตอนนี้ผมขอพุ่งเป้าไปถึง IPC อีกแบบนั่นก็คือ message queue นั่นเอง ถ้าท่านไม่คุ้นเคย ลองนึกตามผมดูครับ  ถ้าเรามีเพื่อนที่เราต้องการสื่อสาร อาจจะอยู่คนละจังหวัด ถ้าเราต้องเดินทางไปเองคงเหนื่อย และถ้าทุกคนต้องทำอย่างนี้กันหมด บขส คงเต็มไปด้วยคนที่ต้องการเพียงแค่สื่อสารอะไรบางอย่างเพียงเล็กน้อยเท่านั้น โลกคงวุ่นวายน่าดู วันๆ คงไม่ต้องทำอะไรกัน ยังดีที่เรามีไปรษณีย์ สิ่งที่ต้องทำก็คือเราก็แค่จ่าหน้าซองเท่านั้น ไปรษณีย์ก็จะนำส่งจากเราไปสู่เพื่อน ถ้าหาไม่เจอตัวก็จะมีกระบวนการทำให้ส่งให้ถึงมือในที่สุด

เทียบเคียงกับโปรเซส ถ้าเราเป็นโปรเซสหนึ่ง แล้วอยากจะส่งข้อมูลอะไรบางอย่างให้อีกโปรเซสหนึ่ง ซึ่งอยู่ในเครื่องคอมพิวเตอร์เดียวกันหรืออาจจะอยู่คนละซึกโลกก็แล้วแต่ การติดต่อโดยตรงโดยอาจใช้ TCP/IP socket ก็ทำได้ แต่ลองนึกภาพดูนะครับ ถ้ามีการเปลี่ยนแปลงเพิ่มลดเครื่องคอมพิวเตอร์ เราต้องวุ่นวายในการแก้ config ต่างๆ ใหม่ ยุ่งยากมากครับ

ทางแก้ก็คือ แทนที่จะให้แต่โปรเซสติดต่อกันเองโดยตรง ก็มาผ่านตัวกลางแทน โดยผู้ส่งนั้นจ่าหน้าซองถึงผลรับแล้วก็ส่งไปยังตัวกลางตัวนี้ และตัวกลางตัวนี้ก็จะรับผิดชอบส่งให้ถึง หรือแม้แต่กระจายไปยังผู้รับหลายๆ โปรเซส ถ้าผู้ส่งต้องการเช่นนั้นก็ย่อมทำได้เช่นกัน ประเด็นสำคัญก็คือ โดยปกติแล้วผู้รับจะ online อยู่กับตัวกลางนี้ เมื่อผู้ส่งส่งข้อมูลมา ก็จะได้รับแทบจะทันที แต่ก็มีบ้างที่ผู้รับไม่พร้อม ด้วยเหตุสุดวิสัยอะไรก็แล้วแต่ เมื่อส่งไม่สำเร็จ ตัวกลางก็จะเก็บข้อมูลนั้นเองไว้ แล้วส่งให้ภายหลังเมื่อติดต่อได้ เหมือนกับไปรษณีย์ไม่มีผิดเลย

ตัวกลางที่ว่านั้นก็คือ message queue นั่นเอง มีลักษณะเป็น server เหมือนกับ SQL Server โดยมากมักติดต่อเชื่อมกันผ่าน TCP/IP ทำให้เราสามารถสื่อสารกันข้ามทวีปกันได้อย่างสบาย ถ้าพูดกันถึงโปรแกรมแล้ว message queue server ที่เก่าๆ ก็มีของ Microsoft ชื่อว่า MSMQ มีมาให้ฟรีอยู่แล้วใน Microsoft Windows ทุกเครื่อง คนที่ไม่ได้เขียนโปรแกรมก็ไม่ค่อยรู้จักกัน หรือถ้าจะเอาตัวดังๆ ตัวอื่นก็มี RabbitMQ หรือ ActiveMQ เป็นต้น

การใช้งาน message queue ยังมีข้อดีอีกข้อหนึ่งครับ บางคนใช้ message queue เพื่อการนี้โดยเฉพาะก็มีครับ นั่นคือ message queue นั้นเป็นอิสระต่อภาษาคอมพิวเตอร์ ไม่ต้องไปสนใจนะครับว่าตัว message queue server นั้นเขียนภาษาอะไร จะเป็น erlang หรือ  go หรืออะไรก็ช่างครับ ไม่เกี่ยวกับเรา เพราะเรารับส่งแค่ message อยู่แล้ว เหมือนกับ SQL Server ครับ เราจะใช้ภาษาอะไรติดค่อเข้าไปก็ถ้า ตราบได้ที่เขาทำ library มาให้เราใช้ โดยที่เราไม่ต้องสนใจว่า SQL Server ตัวนั้นเขียนด้วยภาษาอะไร

แล้วยังไง เราส่ง message ได้แล้วยังไง ถ้าเป็น SQL Server ก็คงไม่กระไรนัก เพราะเราส่งคำสั่ง SQL ไปยัง server และ server ตอบกลับมาเป็นตาราง แต่สำหรับ message queue แล้วลองนึกตามให้ดีๆ นะครับ เราติดต่อ message queue เพื่อส่งเอกสาร แต่เอกสารนั้นไม่ได้ส่งกลับมาหาเรา แต่ส่งไปยังผู้รับอีกที แล้วถ้าผมบอกว่า ผู้ส่งและผู้รับนั้นไม่จำเป็นต้องเขียนด้วยภาษาคอมพิวเตอร์ภาษาเดียวกันหละ หลอดไฟสว่างเลยไหมครับ

ถ้ายังไม่เห็นภาพ ผมขอชี้เป้าให้เห็นชัดเจน เอาอย่างนี้ครับ จากที่เราเรียนรู้มาเรื่อง library GMP ที่เอาไว้หาคำนวณตัวเลขจำนวนเต็มขนาดใหญ่ ซึ่งมีใน C/C++ เท่านั้น (สมมุตินะครับ) เกิดเราต้องการเขียนโปรแกรมอะไรบางอย่างที่ต้องมีการคำนวณแบบที่ต้องใช้ GMP แต่ก็มีส่วนติดต่อผู้ใช้ซึ่งเขียนด้วย Visual Basic น่าจะง่ายกว่า แล้วจะทำอย่างไรครับ ถ้าโอนโปรแกรมทั้งหมดมาทำบน Visual Basic ก็อาจจะคำนวณเลขขนาดใหญ่ได้ช้ามากหรือหา library ใช้ไม่ได้เลย แต่ถ้าใช้ C เขียนทั้งหมดรวมทั้งส่วนติดต่อผู้ใช้ก็หนักเอาเรื่อง นั่นแหละครับจึงเป็นที่มาของ message queue เมื่อเวลาที่ต้องการคำนวณ VB ก็จะส่งเลขที่ต้องการคำนวณไปให้ message queue เพื่อส่งต่อไปยัง C เมื่อ C คำนวณเสร็จก็จะส่งคำตอบกลับมายัง VB โดยผ่าน message queue เช่นกัน

สิ่งที่ได้เป็นของแถมก็คือ เมื่อท่านเชื่อมโปรแกรมภาษา C ที่ใช้ GMP เข้าสู่ message queue ท่านจะใช้ภาษาอะไรก็ได้เชื่อมมายัง message queue เพื่อติดต่อกับโปรแกรมภาษา C ของท่านได้ ทำให้โปรแกรมนั้นเป็นอิสระต่อภาษาในทันที ภาษาอะไรก็สามารถคุยภาษาอื่นได้ทั้งหมด ถ้าท่านคุ้นเคยกับ socket ของ TCP/IP ท่านก็อาจจะบอกว่าไม่แปลก ก็ถ้าติดต่อผ่าน TCP/IP มันก็ต้องเป็นกลางทางภาษาอยู่แล้ว ซึ่งนั่นก็ใช่ครับ เพียงแค่ message queue นั้นมันเป็นระดับการเขียนที่สูงกว่า มีความยืดหยุ่นกว่ามาก ก็เท่านั้นครับ

ย้อนกลับมาที่โปรแกรมหาค่า Bernoulli Number ของเรา ถ้าเราแตกโปรแกรมออกเป็นสองส่วน ส่วนที่คำนวณแบบขนานได้คือส่วนที่หาค่า BkModP ผมกระจายไปยัง Banana Pi แต่ละตัวโดยการเขียนโปรแกรมให้ทำงานแบบหลายเทร็ดได้ ให้เต็มประสิทธิภาพ แล้วผมก็เอามาเชื่อมต่อกับ message queue จากนั้นผมก็เอาส่วนในการสั่งงานและส่วนรวบค่ามาไว้ที่เครื่องคอมพิวเตอร์เครื่องหนึ่ง เพื่อติดต่อไปยัง message queue ไปกระจายงานสู่เครื่องอื่นและรวบกลับมาอีกที เท่านี้เราก็จะได้โปรแกรมที่รีดกำลังเครื่องหลายเครื่องเท่าที่เรามีแล้วครับ ซึ่งรายละเอียดเดี๋ยวผมค่อยๆ แจงให้อ่านกัน ใจเย็นๆ ตามผมมา

ZeroMQ นามนี้สำคัญไฉน

ZeroMQ เป็น message queue ตัวหนึ่ง ไม่ใช่สิ! ผมพูดอย่างนั้นก็พูดได้ไม่เต็มปากครับ ถ้าคำว่า message queue หมายถึง server ตัวหนึ่งที่ตั้งขึ้นมาเหมือนกับ SQL Server เพื่อรอทั้งผู้รับและผู้ส่งแล้วหละก็  ZeroMQ  ก็สอบตกเพราะไม่มี server อย่างที่ว่านั้นครับ ZeroMQ ใช้วิธีเชื่อมต่อแบบฝาก server ให้อยู่กับตัวลูกตัวใดตัวหนึ่ง ตัวใดก็เป็น server ได้ บางเวลาเป็น client บางเวลาเป็น server ขึ้นอยู่กับการเขียนของเราครับ คำว่า Zero ในชื่อนั้นก็ได้มาจากการนี้เองครับ คือเราไม่ต้องมี message queue server จริงๆ ซึ่งการทำงานก็ไม่ได้ “ได้งาน” ด้อยไปกว่า message queue ตัวอื่นแต่อย่างใด แต่ในทางกลับกัน ความยืดหยุ่นกลับได้มากกว่าครับ

การติดตั้ง

ZeroMQ รองรับหลากหลาก OS หลักๆ ได้หมด ภาษาคอมพิวเตอร์ใดๆ ที่นิยมใช้กันในปัจจุบันก็น่าจะครบถ้วน ในบทความนี้ผมจะเน้นไปที่ Python และ C++ แต่ C++ นั้นผมจะแสดงให้เห็นเฉพาะบน *buntu เท่านั้น ใครสนใจ OS ตัวอื่นก็ไปศึกษาเพิ่มเติมได้ครับ

Python

ถ้าใครใช้ CPU ตระกูล x86 ผมเคยแนะนำแล้วว่าให้ลงชุด anaconda ของ Continuum Analytics ไม่ว่าจะเป็นบน Windows หรือ Linux ก็ตาม ท่านจะได้ zeroMQ มาในชุดอยู่แล้ว ไม่ต้องลงอะไรเพิ่ม แต่ถ้าท่านลงชุดเล็กคือ conda หรือลง anaconda แต่ต้องการปรับปรุงรุ่นให้เป็นรุ่นใหม่ล่าสุด ก็ให้พิมพ์

แต่ถ้าท่านใช้ *buntu แต่เลือกที่จะไม่ใช้ anaconda เนื่องจาก CPU ของท่านเป็นตระกูล ARM หรือเหตุผลใดก็แล้วแต่ ท่านก็สามารถลง package ได้ดังนี้

แต่การใช้ python-zmq ก็มีปัญหาเล็กน้อยครับ คือบน Banana Pi ที่ใช้ CPU ของ ARM ผมใช้ python3 จะเกิด error ขึ้นแต่ python2 ไม่เป็นไร ส่วนบนเครื่อง PC ไม่มีปัญหาครับ

C++

ใครใช้ *buntu พิมพ์คำสั่งดังนี้ได้เลยครับ

ZeroMQ นั้นแม้ว่าโดยรากฐานแล้วจะสร้างขึ้นมาจากภาษา C++ เป็นหลักนั้น แต่ก็เป็นเพียงกลไกที่อยู่ภายในเท่านั้น ส่วนที่เป็น API ให้เราใช้นั้น ยังคงยึดหลักง่ายๆ ดิบๆ เป็นแบบภาษา C ทั้งนี้ก็เข้าใจไม่ยากครับ เพราะต้องการให้มีความยืดหยุ่นในการ port เป็นภาษาอื่นๆ นั่นเอง ดังนั้นจึงกลายเป็น library ที่ใช้งานยุ่งๆ เล็กๆ

ในภาษา Python ข้างต้น ไม่ต้องห่วงเขาครับ เขาสร้าง interface ใหม่ขี่บน API เดิมแบบภาษา C ทำให้ใช้งานง่ายมาก ส่วนภาษา C++ นั้นมีทางเลือก 3 ทางครับ

  • ทางแรกก็คือใช้ library ภาษา C เขียน C ใน C++ เลย แบบนี้ทำได้อยู่แล้ว แต่ก็ต้องเขียนยุ่งๆ แบบภาษา C
  • ไปใช้ cppzmp ซึ่งเป็น header C++ ขี่บน C library อีกที ถ้าใช้ก็ไม่ได้อะไรมากนัก เป็น interface บางๆ ขี่บนภาษา C การเขียนโปรแกรมนั้นก็ไม่แตกต่างภาษา C เท่าใดนัก
  • ใช้ library zmqpp เป็น library ระดับสูงขี่บน API ภาษา C ดึงความสามารถของ C++ มาใช้ทำให้ใช้งานง่ายขึ้นมาก แต่ก็มีข้อเสียคือประสิทธิภาพลดลงเล็กน้อย (ไม่น่าเกิน 5%) ผมเลือกใช้วิธีนี้

เหตุผลหลักที่ผมใช้ zmqpp นั้นก็เป็นเพราะ ZeroMQ มาตรฐานที่เขียนขึ้นมาจาก C++ รองรับการส่งข้อมูลแบบ string เท่านั้นเพื่อให้รองรับภาษาคอมพิวเตอร์ต่างๆ ได้ง่ายนั่นเอง แต่ string ที่ว่านั้น ส่งเป็นรูปแบบแบบภาษา Pascal นั่นคือ ส่งจำนวนตัวอักษรเป็นไบต์แรก และค่อยตามด้วยข้อความตามจำนวนตัวอักษรที่กำหนดนั้น ซึ่งเมื่อเราใช้ภาษา C/C++ ที่ string เป็นแบบจบตัวตัวอักษร ‘\0’ ก็เลยต้องมาแปลงก่อนส่ง วุ่นวายพอสมควร ผมเลยใช้ zmqpp ที่จัดการให้เราเสร็จสรรพ เราใช้แบบ stream แบบ C++ cin, cout ได้เลย ซึ่งสะดวกกว่า และส่วน Python ไม่ต้องห่วงเขาครับ เขาทำให้ง่ายอยู่แล้ว เราใช้งานโดยรู้สึกเหมือนเป็น string ธรรมดาของ Python เลย ที่เหลือเขาบังความซับซ้อนเอาไว้

โมเดล push-pull ใน ZeroMQ

การใช้งาน ZeroMQ นั้นมีหลายรูปแบบมาก ถ้าจะคุยกันให้ครบถ้วนก็คงต้องเขียนเป็นบทความชุดหนึ่งกันเลยทีเดียว เพื่อให้ย่นย่อเหมาะกับบทความแค่บทเดียว ผมจึงนำเสนอเฉพาะโมเดลการใช้งานที่ต้องสนองกับโจทย์ที่เราทำมาแค่โมเดลเดียว ซึ่งนั่นก็คือโมเดล push-pull ดังมีรายละเอียดดังภาพนี้ครับ

hpc10_1

จากภาพคือโมเดล push-pullการทำงานเป็น แบบหนึ่งต่อหลาย (one-to-many)  กล่องสี่เหลี่ยมแต่ละกล่องมองเป็นเครื่องคอมพิวเตอร์แต่ละเครื่อง เส้นเชื่อมก็คือเน็ตเวิร์คในระบบ TCP/IP นั่นเอง ดังนั้นทุกกล่องสี่เหลี่ยมจะมี IP address เป็นของตัวเอง และตัวที่เป็นหนึ่งนั้น ถ้ามองว่าเป็น server ก็อาจไม่ถึงกับผิดอะไรมากมายนัก ตัวที่เป็นหนึ่งจะต้องใช้คำสั่ง bind และต้องกำหนด port เพื่อรอให้ตัวหลายมาเชื่อมต่อ ส่วนตัวที่เป็นหลายนั้นไม่ต้องกำหนด port ครับ เป็นธรรมชาติของ TCP/IP อยู่แล้วที่ OS จะเป็นคนเลือก port ที่เชื่อมต่อให้ไม่ซ้ำกันโดยอัตโนมัติ

การส่งข้อมูลนั้นจะทำได้ทางเดียว จาก push ไปสู่ pull เท่านั้น จะส่งจาก “หนึ่ง” ไปสู่ “หลาย” หรือส่งจาก “หลาย” ไปสู่ “หนึ่ง” ก็ได้ โดยยึดหลักที่กล่าวไปแล้วคือจาก push ไปสู่ pull เป็นหลัก และสายเส้นหนึ่งที่เชื่อมนั้นทำงานได้ในทิศทางเดียวเท่านั้น

ZeroMQ ก็มีความสามารถพื้นฐานเหมือนกัน message queue ทั่วไปคือ ถ้ามีการ push ข้อมูลมาแล้ว แต่ตัว pull ไม่พร้อมรับ อาจจะแฮงค์เมื่อบูทใหม่ ก็สามารถมารับทีหลังได้ทำให้การรับส่งข้อมูลมีความน่าเชื่อถือมากขึ้น

ทดสอบโปรแกรมโดยใช้โมเดล push-pull

โมเดลการเขียนโปรแกรมของเราเมื่อเทียบกับ SQL Server ก็จะไม่เหมือนกันนัก กล่าวคือ SQL Server นั้นตั้งขึ้นมาเป็น “หนึ่ง” เป็นตัวประมวลผล ซึ่งรอ client ที่เป็น “หลาย” ติดต่อส่งโจทย์มาให้ “หนึ่ง” ประมวลผล แต่ในงานของเรานั้น อาจจจะกลับกันในบางมิติ ตัวที่ประมวลผลนั้นถูกตั้งขึ้นมาเป็น “หลาย” เพื่อรอ “หนึ่ง” ติดต่อเข้ามา และเมื่อ “หลาย” ประมวลผลเสร็จ ก็จะส่งข้อมูลกลับให้ “หนึ่ง” รวบรวม เพื่อความเข้าใจ ดูเป็นภาพง่ายกว่า ดังนี้

hpc10_2

จากภาพจะเห็นได้ว่า ทุกเครื่องทำหน้าที่เป็นทั้ง push/pull ในตอนแรก ตัว “หนึ่ง”  bind port 6666  (ตัวเลขอะไรก็ได้ครับที่ไม่ซ้ำกับโปรแกรมอื่นที่จองเอาไว้ เพื่อความปลอดภัย เกิน 2000 ไว้ก่อนครับพ่อสอนไว้) เพื่อ push งานไปสู่ worker แต่ละตัว เมื่อ worker ได้รับงานก็จะเอาไปประมวลผล เมื่อทำงานเสร็จก็จะ push กลับไปสู่ “หนึ่ง” และ “หนึ่ง” จะรวบรวมสิ่งที่ได้จาก queue นำมาประมวลผลเป็นคำตอบ

ผมลองเขียนโปรแกรมทดสอบโดยแบ่งออกเป็นสองส่วน คือส่วนของ shell และ worker ตัว shell นั้น จะรอรับค่าจากผู้ใช้ เมื่อผู้ใช้ป้อนค่าแล้วเช่น 10 ตัว shell จะกระจาย 1 ถึง 10 ไปยัง worker ต่างๆ ที่เชื่อมอยู่ ตัว worker แต่ละตัวเมื่อได้รับค่าจาก shell มาแล้ว ก็เอามาคูณด้วย 10 แล้วก็ส่งคืนมายัง shell เมื่อ shell ได้รับแล้ว ก็เอามาบวกรวมกัน กล่าวโดยสรุปในมุมมองของคณิตศาสตร์ ก็คือการบวกเลขตั้งแต่ 1 ไปจนถึงค่าที่ผู้ใช้ป้อน แล้วเอาผลลัพธ์มาคูณ 10 นั่นเอง ผมเขียนโปรแกรมทดสอบทั้ง Python และ C++ ให้เห็นการทำงานของ ZeroMQ เรามาดูกันเลยครับ

การเขียนโปรแกรมเชื่อมสาย

เรามาดูในส่วนของโปรแกรม shell ก่อน ดูเฉพาะในส่วนหัว Python ดังนี้

 ส่วนภาษา C++ ดังนี้

ตรงไปตรงมาเหมือนกับรูปเลย คงไม่ต้องอธิบายมาก เขียนง่ายๆ แค่นี้เองครับ และในโปรแกรม worker เขียนดังนี้ครับ

 และในภาษา C++ ดังนี้

ก็เช่นกันตรงไปตรงมา

การทำงาน shell

ดูโค๊ดกันเลยครับ ภาษา Python กันก่อน

โปรแกรมนี้ที่เห็นทำการวนรอบไม่รู้จบ โดยการสร้าง prompt “>>> ” ไว้รอเราพิมพ์ข้อมูลเข้า ข้อมูลที่เราพิมพ์เข้าไป จะกลายเป็นตัวแปรตัวเลข max ในบรรทัดที่ 13  จากนั้นบรรทัดที่ 14-15 นั้นเป็นการวนรอบตั้งแต่ 1 ถึง max ส่งค่าตัวเลขนี้ไปยัง workers เป็นแบบ string (เพื่อให้รองรับภาษาคอมพิวเตอร์ได้หลากหลาย) จากนั้น ก็วนรอบรับคำตอบ ในบรรทัดที่ 18-21  เอาผลลัพธ์ที่ workers คำนวณกลับมาสะสมค่ารวมกันที่ sum เมื่อครบทุกตัวแล้วก็แสดงผลลัพธ์คำตอบ ถ้าเป็นภาษา C++ ก็จะเป็นดังนี้ครับ

 ทดสอบรันโปรแกรม

โปรแกรมในส่วนของ worker นั้นคงไม่ต้องอธิบายอะไรมากมายนะครับ เพราะใช้หลักการเดียวกับกันตัว shell ดังที่ผมกล่าวมา ตัว worker ที่เขียนโปรแกรมที่มีการ “connect” นั้นจะเปิดกี่ตัวก็ได้ครับในเครื่องเดียวกัน แต่ตัวที่ “bind” หนึ่งเครื่องเปิดได้ตัวเดียว ในงานของเราทั้งวง Lan มีแค่เครื่องเดียวครับที่ “bind”  ผมแสดงโปรแกรมตัวเต็มPython / C++ และทั้ง shell/worker ดังนี้ครับ

shell ของ Python  HPCThai/introHPC/10/shell.py
worker ของ Python HPCThai/introHPC/10/worker.py
shell ของ C++ HPCThai/introHPC/10/shell.cpp
worker ของ C++ HPCThai/introHPC/10/worker.cpp

ผมทดสอบรันโปรแกรมบน Kubuntu บนเครื่อง PC โดยให้ worker เป็น Python และ shell เป็น C++ ดังนี้ครับ

worker

โปรแกรมก็จะค้างรอ ไม่หลุดกลับมาที่ shell ถูกต้องแล้วครับ จากนั้นก็รันโปรแกรมภาษา C++ จะได้ผลลัพธ์ดังนี้ครับ

shell

โปรแกรมนี้จะใช้ shell และ worker ต่างภาษากันได้หมดครับ เพราะ ZeroMQ เป็นกลางทางภาษานั่นเอง

คำถามคาใจ

โปรแกรมในหัวข้อที่ผ่ามมา ดูง่ายๆ ธรรมดาๆ ดีใช่ไหมครับ แต่ถ้าใครคิดลึกลงไปอีกหน่อย สมองจะเต็มไปด้วยคำถามครับ เรามาดูข้อสงสัยกันครับว่ามีอะไรบ้าง

โค๊ตจัดการ worker อยู่ที่ไหน

คำถามแรกก็น่าจะเป็นคำถามนี้ครับ “โค๊ตจัดการ worker อยู่ที่ไหน” โดยปกติแล้วถ้ามี worker ติดต่อเข้ามา ระบบอื่นอาจจะบรรจุหัวขั้วเอาไว้ใน array จะได้วนรอบเพื่อส่งข้อมูล หรือไม่ก็จะสร้างเทร็ดใหม่ขึ้นมาอีกเทร็ดเอาไว้คุยกับ worker ตัวนั้นเลย แล้วโค๊ตที่ว่านั้นอยู่ตรงไหน ไม่เห็นมีเลย คำตอบก็ง่ายๆ เลยครับ ไม่เห็นก็คือไม่มี ก็แค่นั้นครับ

ZeroMQ จะจัดการ worker ที่มา “connect” เข้าสู่ “bind” ของ shell อยู่หลังฉากโดยที่เราไม่ต้องรับรู้ครับ แม้กระทั่ง worker หรือ shell ใครจะรันก่อนหรือหลังไม่เป็นประเด็นครับ ZeroMQ ถือเอาเวลาที่กำลังส่งข้อมูลเป็นหลัก เหลือ worker ตัวไหนบ้างก็ส่งให้เท่าที่มีก็แค่นั้น

การที่เราไม่เข้าไปรับรู้เรื่องการเชื่อมตัวของ worker ถือว่าเป็นจุดดีเด่นอย่างมาก นอกเสียจากเราไม่ต้องวุ่นวายกับการเขียนโปรแกรมแล้ว เรายังสามารถเขียนโปรแกรมในระดับที่สูงขึ้นได้อีกด้วย

โค๊ตตรงที่กระจายงานอยู่ตรงไหน

ถ้าพิจารณา HPCThai/introHPC/10/shell.py ในบรรทัดที่ 14-15 ดังนี้

พิจารณาดีๆ ก็ชวนให้สงสัยนะครับ เราไม่ได้ระบุว่าจะส่งตัวเลขใดต่างๆ ไปยัง worker ตัวใด แบบนี้ทุกเครื่องก็จะได้ตัวเลขทุกตัวทั้งหมดใช่หรือไม่ ถ้าเป็นแบบนั้นก็ทำงานซ้ำซ้อนแย่เลย อย่ากระนั้นเลยครับ ผมจะลองเอาโปรแกรมไปรันบน Banana Pi 3 เครื่องเพื่อให้เห็นการทำงาน คราวนี้ลองเปลี่ยนดูบ้างครับ เอา C++ เป็น worker และเอา Python เป็น shell บ้าง โดยที่ตัว shell นั้นใช้เครื่อง node01 ของ Banana Pi นั่นเอง ดังนั้นในตัว worker แต่ละตัวต้องเปลี่ยนตัว connect ทั้งสองตัว จาก localhost:6666 และ localhost:7777 มาเป็น node01:6666 และ node01:7777 ตามลำดับ เมื่อแก้เรียบร้อยแล้ว ก็คอมไพล์ worker.cpp แล้วรันครับ มันจะค้างอยู่ แล้วผมก็เข้าเครื่อง node01 เพื่อเรียก ipython shell.py เพื่อส่ง 10 ไปให้ประมวลผล ซึ่งจะได้ผลลัพธ์ดังนี้ครับ

 shell_worker_3

 หน้าจอพื้นสีดำเป็น worker ทั้ง 3 ตัว ส่วนจอพื้นสีขาวเป็น node01 เรียกใช้ shell.py สิ่งที่น่าสนใจก็คือดูครับว่า worker แต่ละตัวได้รับค่าอะไร และส่งอะไรกลับ ที่ส่งกลับนั้นก็รู้ๆ กันว่าคูณ 10 จากค่าที่ส่ง ไม่มีอะไรเป็นพิเศษ แต่สิ่ง push จาก shell มาสู่ worker สิครับน่าสนใจ ข้อมูลเป็นแบบกระจายแจกแบบ round robin ไม่มีการส่งข้อมูลซ้ำในแต่ละ worker นะครับ

ถามว่าผมทำอะไรเป็นพิเศษไหม ก็ตอบว่าไม่ครับ มันเป็นธรรมชาติของ ZeroMQ ที่ทำงานแบบ push-pull จะกระจายข้อมูลแบบ cyclic partitioning อยู่แล้ว  คงพอจะได้คำตอบแล้วนะครับว่าทำงานงานอย่างไร

การประยุกต์ใช้งาน Bernoulli Number

เพื่อความสอดคล้องกับเนื้อหาข้างต้น ผมเลยขอปรับโปรแกรมที่ใช้หาค่า Bernoulli Number ในบทก่อนหน้า มาทำงานเป็นแบบ shell/worker ซึ่งโปรแกรมเดิมจะถูกแตกออกเป็นสองส่วน ส่วนที่เป็น shell และส่วนที่เป็น worker เหมือนเดิมครับ ผมจะให้ worker เป็นเครื่อง node01, node02 และ node03 ส่วน shell จะอยู่ที่ node01 ครับ โดยใช้ทั้ง Python และ C++ เหมือนเดิม แต่ตอนทดลองผมจะไม่ไขว้นะครับ จะได้เปรียบเทียบเวลาที่ใช้กับบทก่อนได้

ผมยึดหลักแก้โปรแกรมเดิมน้อยที่สุด โดยแก้เฉพาะในส่วนฟังก์ชัน distribute() และ worker() โดยกระจายส่วนของ worker() ไปยัง workers ต่างๆ และในส่วนของ distribute() จะอยู่ในส่วนของ shell  จากนั้นก็เพิ่มในส่วนของ main() โปรแกรมทั้งสองส่วน ก็ลอกเอาจากโปรแกรมทดสอบข้างบนมาใช้นั่นเอง

หลักการในการทำตัว worker นั้น ผมใช้วิธีการสร้างเทร็ดตามจำนวนคอร์ของ CPU ถ้าเป็น Python ผมจะใช้ library multiprocessing ดังนี้ครับ

จากโปรแกรมเราจะเห็นนะครับ ในบรรทัดที่ 112 เป็นการหาว่า CPU ของเรามีกี่คอร์ จากนั้นก็สร้าง worker() ขึ้นมาตามจำนวนคอร์ครับ วนรอบไปตลอดกาล เพื่อรอรับงานจาก shell มีสิ่งหนึ่งน่าสนใจครับคือ การคำนวณ computeBkModP() ได้นั้นต้องอาศัยค่า k ด้วย เพื่อความเรียบง่าย ผมก็เลยส่งค่า k มาใหม่ทุกครั้งที่ส่งค่า p (ในที่นี้คือ pp) แม้ดูว่าซ้ำซ้อนแน่ก็ไม่น่าจะเสียประสิทธิภาพอะไรมากมายนัก วุ่นวายน้อยกว่าการสร้างโปรโตคอลให้ส่งค่า k มาเก็บไว้ล่วงหน้า  ผมเอาง่ายๆ แบบนี้ครับ ข้อมูล push-pull สรุปเป็นได้ดังนี้ครับ

hpc10_3

เมื่อ worker ได้ k/p จาก shell แล้วก็นำไปคำนวณค่า BkModP เมื่อคำนวณเสร็จก็ส่ง  BkModP/p  กลับสู่ shell ซึ่งโปรแกรม shell เองก็ไม่มีเทร็ดเพิ่มแต่อย่างใดครับ ทำงานที่เทร็ดเดียวตลอด รายละเอียดผมคงไม่แจงนะครับ ไปอ่านในโปรแกรมเองได้ ไม่ซับซ้อนดังนี้ครับ

bern_shell ของ Python  HPCThai/introHPC/10/bern_shell.py
bern_worker ของ Python HPCThai/introHPC/10/bern_worker.py
bern_shell ของ C++ HPCThai/introHPC/10/bern_shell.cpp
bern_worker ของ C++ HPCThai/introHPC/10/bern_worker.cpp

การทดสอบโปรแกรม

ถึงเวลาทดสอบแล้ว ผมเองก็ยังไม่ได้ลอง รอทดสอบไปพร้อมๆ กับที่เขียนบทความนี้ครับ ผมขอเริ่มที่ python ก่อน ใช้งานทั้ง shell.py และ client.py ทั้งคู่ อย่าลืมแก้ worker แต่ละตัวให้ connect มายังเครื่อง shell ก่อนนะครับ ในที่นี้คือ node01 นั่นเอง ทดลองกันที่ B(1000) อย่างเดียวเลยครับ ได้ผลดังนี้

py_bern_worker3

 

หรือจะดูเฉพาะ shell.py เต็มๆ ก็ได้ดังนี้ครับ

เรายังไม่วิเคราะห์อะไรมากมาย แต่สังเกตว่าคำตอบนั้นถูกต้อง เอาเท่านั้นก่อน เราขยับไปกันที่ C++ ก่อนครับดังนี้

cpp_bern_worker3

 

หรือดู bern_shell ภาษา C++ แบบเต็ม ดังนี้

 การวิเคราะห์ผล

จำกฏของอู๋ได้ไหมครับ ผมจะนำเอาวิเคราะห์ประสิทธิภาพ ค่าตามกฏของอู๋ ยิ่งเข้าใกล้หนึ่งมากเท่าใด ก็ยิ่งถือว่ามีประสิทธิภาพมากขึ้นเท่านั้น ถ้าเป็นหนึ่งถือว่าเป็นอุดมคติซึ่งแทบเป็นไปไม่ได้เลย

เรามาดูที่ Python กันก่อน Python นั้นเมื่อทำงานบนเครื่องเดียวคอร์เดียว ผมเคยทดสอบแล้วใน ก้าวแรก HPC #06: อัลกอริทึมประสิทธิภาพสูงและการปรับปรุงประสิทธิภาพ ใช้เวลา 1:45m หรือ 105s แต่มาครั้งนี้ใช้ 6 คอร์ ทำได้ที่ 37.89s  เมื่อใช้กฏของอู๋จาก ก้าวแรก HPC #07: เตรียมเสบียงเลี้ยงตัว สู่โลกแห่งการประมวลผลแบบขนาน คำนวณดูแล้วพบว่า

\frac{N(T_{1}-T_{N})}{T_{1}(N-1)}=\frac{6(105-37.89)}{105(6-1)}\approx 0.767

ส่วนภาษา C++ นั้นทำงานที่คอร์เดียวใช้เวลา 49.532s พอมาใช้งาน 6 คอร์เหลือเพียง 9.447s  เมื่อเข้าสู่กฏของอู๋ด้วยวิธีเดียวกันจะได้

\frac{N(T_{1}-T_{N})}{T_{1}(N-1)}=\frac{6(49.532-9.447)}{49.532(6-1)}\approx 0.971

การที่ได้ค่า 0.767  ที่ Python ทำได้นั้น นั้นถือว่าเป็นค่าที่ดี ตัวเลขขนาดนี้ถือว่าการแตกเทร็ดและการเชื่อมต่อเน็ตเวิร์คถือว่ามีประสิทธิภาพค่อนข้างดีเลยทีเดียว แต่สำหรับ C++ ที่ได้ถึง 0.971 นี่ไม่ธรรมดานะครับ เลิศล้ำมาก ปกติแล้วเรามักจะไม่ได้ตัวเลขที่ดีขนาดนี้ครับ การได้ตัวเลขขนาดนี้อาจกล่าวได้ว่าเน็ตเวิร์คที่ว่าช้ามากทำอะไร C++ ไม่ได้ อย่างนี้ก็คงบอกได้ว่า ZeroMQ บน C++ เขียนไว้ได้อย่างมีประสิทธิภาพมาก แม้ว่าเราจะใช้ zmqpp มาเป็นฉากหน้าเพื่อลดความซับซ้อนแต่ในเวลาเดียวกันก็ลดประสิทธิภาพลงก็ตาม ก็ยังเร็วมากมายอยู่ดี และการกระจายเทร็ดของ C++ นั้นทำได้อย่างสมบูรณ์เป็นแบบ embarassingly parallel  ไม่มีการจัดจราจรมารบกวนประสิทธิภาพ ลืมบอกไปว่าผมใช้ 1GB Lan ในการเชื่อมต่อเครื่องแต่ละเครื่องเข้าด้วยกันครับ

มีประเด็นหนึ่งครับที่ละเลยไม่ได้สำหรับ Python มีจุดหนึ่งครับที่ผมไม่เคยกล่าวถึงเลย ก็คือภาคแสดงผลของ B(1000) ที่แสดงตัวเลข -18243…9030 นั่นแหละครับ ตัวนี้ภายในแล้วต้องแปลงจากตัวเลขแบบจำนวนเต็มขนาดใหญ่มาเป็น string ก่อนค่อยนำมาแสดงผล จุดนี้เองครับ Python ทำได้ไม่ดีนัก เสียไปหลายวินาทีเลยทีเดียว แต่ส่วนของ C++ นั้นทำงานเร็วมาก ตามกฏของแอมดาลอยู่แล้ว เมื่ออัตราส่วนของการประมวลผลแบบอนุกรมเพิ่มขึ้น ประสิทธิภาพโดยรวมของระบบจะลดลงฮวบ ทำให้เสียคะแนนในกฏของอู๋อย่างชัดเจนอย่างที่เห็น

ยังมีประเด็นเล็กๆ ที่ผมละเลยไม่ได้พิจารณาก็คือ การรันโปรแกรมแบบเริ่มต้นจาก OS shell เทียบกับที่ผมปรับมารันรอเป็น prompt >>> ก็จะมีเวลาเริ่มต้นก่อนรันที่ต่างกันเล็กน้อย และอีกประเด็นก็คือ ผมใช้ node01 เป็นทั้งตัว shell/worker ดังนั้น latency การรับส่งข้อมูลใน ZeroMQ ใน node01 จะน้อยกว่า node02 และ node03 จึงมีความคลาดเคลื่อนอีกเล็กน้อย แต่ตอนที่ผมใช้ Apache Spark ผมก็ทำลักษณะนี้เช่นกัน ก็เลยไม่แตกต่าง

ทิ้งท้าย

บทความนี้ผมนำเสนอการรีดประสิทธิภาพของเครื่องคอมพิวเตอร์โดยทลายกำแพงของขอบเขตเครื่องคอมพิวเตอร์และภาษาคอมพิวเตอร์ลง จับแต่ละเครื่องมาทำงานร่วมกันเป็น worker โดยใช้ ZeroMQ ได้ ซึ่งผลลัพธ์นั้นอยู่ในเกณฑ์ที่ดีมาก ก็ถือได้ว่าครบถ้วนแล้วนะครับสำหรับคำว่า “ก้าวแรก”

[Total: 3    Average: 5/5]

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *