Big Data Analytics · บทที่ 6 จาก 13

Superlinear Speedup

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

📚
ความตั้งใจของบท: ใช้ประสบการณ์งาน Bioinformatics ปี 2002 เป็นประตูเข้าสู่ Speedup, Efficiency, Cache และ Memory Hierarchy พร้อมฝึกแยกว่า Superlinear ที่พบเกิดจากการออกแบบที่ดีจริง หรือเป็นเพียงผลจาก baseline และการวัดที่ไม่ยุติธรรม

1เรื่องจริงที่ทำให้ผมจำคำว่า Superlinear Speedup มาจนวันนี้

เมื่อปี 2002 ผมใช้ Parallel Algorithm แก้ปัญหา Bioinformatics ที่มี Dynamic Programming เป็นแกน งานแบบนี้สร้างและอ้างข้อมูลในตารางขนาดใหญ่ หากจัดวางข้อมูลหรือแบ่งงานไม่เหมาะ หน่วยประมวลผลไม่ได้เสียเวลาที่สูตรคำนวณเป็นหลัก แต่เสียเวลารอข้อมูลจากหน่วยความจำระดับที่ช้ากว่า

โปรแกรมแบบลำดับใช้เวลาประมาณ 7.5 วัน เมื่อปรับเป็นแบบขนานบนเครื่องที่มี CPU เพียง 8 ตัว เวลาลดลงเหลือประมาณ 8 ชั่วโมง

คำนวณจากตัวเลขที่วัดได้
T₁ = 7.5 วัน = 180 ชั่วโมง
T₈ = 8 ชั่วโมง
Speedup S₈ = T₁ / T₈ = 180 / 8 = 22.5 เท่า
Efficiency E₈ = S₈ / 8 = 2.8125 หรือ 281.25%

ตัวเลข 281.25% ฟังดูเหมือนเครื่องแต่ละตัวทำงานเกินกำลังของตนเอง แต่ความหมายที่ถูกคือ baseline แบบลำดับแบก overhead บางอย่างไว้มาก พอแบ่งงาน working set ของแต่ละ CPU เล็กลงและเข้ากับ Cache ได้ดีขึ้น overhead นั้นลดลงอย่างมหาศาล เราจึงได้ทั้งประโยชน์จากการทำงานพร้อมกันและประโยชน์จาก Memory Hierarchy

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

ยี่สิบปีผ่านไป แนวคิดเดิมเดินทางไปไกลกว่าเครื่องเดิม

ต่อมาแนวคิดนี้ถูกนำไปสอนในวิชา Big Data Analytics ให้กับนักศึกษาสาขาสถิติ นักศึกษาคนหนึ่งนำหลักเดียวกันไปแก้ปัญหา Dynamic Programming ที่ใหญ่กว่ามากบน Apache Spark และลดเวลาจากกว่าหนึ่งปีเหลือประมาณ 13 ชั่วโมง

เรื่องที่น่าสนใจไม่ใช่การนำโค้ดเก่าไปวางบนเครื่องใหม่ เพราะสถาปัตยกรรมและ framework ต่างกันมาก สิ่งที่ถูกส่งต่อคือ วิธีมองปัญหา: อย่าดูแต่จำนวน operation ให้ดูด้วยว่าข้อมูลเคลื่อนผ่าน Cache, RAM, Disk และ Network อย่างไร และถามว่าการแบ่งงานสามารถเปลี่ยนระดับของหน่วยความจำที่ข้อมูลใช้งานอยู่ได้หรือไม่

แต่เรื่องเล่าที่ดีต้องมาพร้อมคำถามที่เข้มงวด

ก่อนประกาศว่าได้ Superlinear Speedup เราต้องตรวจว่าโปรแกรมสองแบบตอบโจทย์เดียวกัน ใช้ input เดียวกัน และวัดขอบเขตเวลาเท่ากันหรือไม่ โปรแกรมแบบขนานอาจเปลี่ยนอัลกอริทึม ใช้ approximation หรืออ่านข้อมูลที่ cache ไว้แล้ว ขณะที่ baseline อ่านจากดิสก์เย็น ๆ หากไม่แยกปัจจัยเหล่านี้ ตัวเลขอาจน่าตื่นเต้นแต่สรุปอะไรไม่ได้

Superlinear เป็นจุดเริ่มต้นของการสืบสวน ไม่ใช่จุดจบ
เมื่อวัดได้เร็วกว่าจำนวน CPU สิ่งแรกไม่ใช่รีบฉลองหรือรีบปฏิเสธ แต่ถามว่า “งานส่วนไหนหายไป ต้นทุนระดับใดลดลง และ baseline สองฝั่งเทียบกันอย่างยุติธรรมหรือไม่” ถ้าตอบได้ Superlinear จะกลายเป็นความรู้ด้านสถาปัตยกรรม ถ้าตอบไม่ได้ มันยังเป็นเพียงตัวเลข

2นิยาม Speedup, Efficiency และ Cost ให้ชัดก่อน

Speedup
เมื่อใช้หน่วยประมวลผล (p) ตัว Speedup นิยามเป็นอัตราส่วนระหว่างเวลาของ baseline แบบลำดับ (T_1) กับเวลาแบบขนาน (T_p): (S_p = T_1/T_p)
Sₚ = T₁ / Tₚ
Eₚ = Sₚ / p
Cₚ = p × Tₚ
กรณีความหมายสิ่งที่ควรตรวจต่อ
Sₚ < pSublinear speedup ซึ่งพบได้ตามปกติส่วนลำดับ การสื่อสาร การรอ และ load imbalance
Sₚ ≈ pLinear speedupช่วงขนาดปัญหาที่ยังรักษาได้ และต้นทุนรวมเพิ่มเท่าใด
Sₚ > pSuperlinear speedupMemory hierarchy, algorithmic change, search anomaly, baseline และ measurement

Efficiency เกิน 100% ไม่ได้แปลว่า CPU สร้างเวลาเพิ่ม

Efficiency เป็นตัวชี้วัดเทียบกับ baseline ไม่ใช่ประสิทธิภาพทางพลังงานหรืออัตราการใช้ CPU โดยตรง เมื่อ baseline เสียเวลารอ memory มาก แต่แบบขนานลดการรอนั้นได้ ค่า (E_p) จึงเกิน 1 ได้ สิ่งที่เกิน 100% คือ “ประโยชน์เมื่อเทียบกับการหารงานตามจำนวน CPU” ไม่ใช่กฎทางกายภาพของ CPU

Cost บอกว่าเร็วขึ้นด้วยทรัพยากรเท่าใด

โปรแกรมที่ใช้ 100 เครื่องและจบใน 1 นาทีอาจเร็วมาก แต่ใช้ทรัพยากรรวม 100 machine-minutes ขณะที่เครื่องเดียวใช้ 50 นาที โปรแกรมแรกมี latency ดีกว่าแต่ cost สูงกว่า การประเมิน Big Data จึงควรดูทั้งเวลาจบ ค่าใช้จ่าย และปริมาณงาน ไม่ควรยก Speedup เป็นคำตอบเดียว

Strong Scaling กับ Weak Scaling

การทดลองสิ่งที่ตรึงไว้คำถาม
Strong Scalingขนาดปัญหารวมคงเดิมเพิ่มเครื่องแล้วงานเดิมจบเร็วขึ้นเท่าใด
Weak Scalingงานต่อเครื่องใกล้เคียงเดิมเพิ่มเครื่องพร้อมเพิ่มข้อมูลแล้วเวลายังคงอยู่ได้หรือไม่

Superlinear ที่เกิดจาก working set พอดี Cache มักปรากฏใน strong-scaling บางช่วง เพราะเมื่อเพิ่มเครื่องจน partition ข้าม “หน้าผา” ของ Memory Hierarchy เวลาลดลงกะทันหัน แต่ถ้าเพิ่มเครื่องต่อไป หลัง working set พอดีแล้ว ผลตอบแทนอาจกลับมาเป็น linear หรือต่ำกว่า linear

3ปัญหาตั้งต้น: การจับคู่ทุกค่าใน x กับทุกค่าใน y

โจทย์
สมมติต้องคำนวณ z[i][j] = functionof(x[i], y[j]) สำหรับทุกคู่ (i, j) โดย x มีขนาด n และ y มีขนาด m
for (int i = 0; i < n; i++)
  for (int j = 0; j < m; j++)
    z[i][j] = functionof(x[i], y[j]);

จำนวนคู่ที่คำนวณคือ O(nm) และจะเขียนเป็น O(n²) ได้เมื่อสมมุติว่า n กับ m โตในระดับเดียวกัน แต่ Big-O บอกจำนวนงานเชิงนามธรรม ยังไม่บอกว่าแต่ละ iteration รอข้อมูลจาก register, cache, RAM หรือ disk นานเพียงใด

Operation Count เท่ากัน เวลาจริงอาจต่างกันมาก

โปรแกรมสองตัวทำการคูณและบวกเท่ากัน แต่ตัวหนึ่งอ่านข้อมูลเรียงต่อเนื่อง ส่วนอีกตัวกระโดดข้าม memory จน cache miss ตลอด ทั้งคู่มี Big-O เท่ากัน แต่เวลาจริงต่างกันหลายเท่า นี่เป็นเหตุผลที่การวิเคราะห์อัลกอริทึมกับการวิเคราะห์สถาปัตยกรรมต้องอ่านควบคู่กัน

สิ่งที่ Big-O ช่วยตอบสิ่งที่ Big-O ไม่ได้ตอบตรง ๆ
จำนวน operation โตตาม input อย่างไรCache miss ต่อ operation เท่าใด
เปรียบเทียบแนวโน้มเมื่อขนาดโตข้อมูลอยู่ในหน่วยความจำระดับใด
ตัด constant factor เพื่อมองรูปการเติบโตconstant factor เปลี่ยนกะทันหันเมื่อ working set ข้าม cache หรือ RAM หรือไม่

Working Set คือข้อมูลที่ต้องใช้ในช่วงหนึ่ง

ข้อมูลทั้งหมดอาจใหญ่หลายร้อยกิกะไบต์ แต่ถ้าในช่วงหนึ่งโปรแกรมใช้จริงเพียง block เล็ก ๆ Working set อาจพอดี Cache ในทางกลับกัน dataset ไม่ใหญ่มาก แต่เข้าถึงแบบกระโดดจน reuse ไม่ทัน Working set ที่มีผลต่อ locality อาจใหญ่กว่าที่คิด การแบ่งงานที่ดีจึงแบ่งตามรูปการเข้าถึง ไม่ใช่แบ่งจำนวน record อย่างเดียว

Temporal และ Spatial Locality

Cache ดึงข้อมูลเป็น cache line ไม่ใช่ทีละตัวแปร การจัด layout และ loop order ให้เดินตามหน่วยความจำต่อเนื่องจึงลด miss ได้ แม้จำนวน operation ทางคณิตศาสตร์ไม่เปลี่ยน

Big-O เหมือนนับจำนวนครั้งที่หยิบหนังสือ
นักศึกษาสองคนต้องหยิบหนังสือหนึ่งพันครั้งเท่ากัน คนแรกวางหนังสือที่ใช้บ่อยไว้บนโต๊ะ คนที่สองเก็บทุกเล่มไว้คนละชั้นของอาคาร จำนวนครั้งยังเป็นหนึ่งพันเหมือนกัน แต่เวลารวมไม่ใกล้กันเลย Memory hierarchy ทำหน้าที่เหมือนระยะทางจากมือไปถึงชั้นหนังสือ

4เมื่อ Working Set ใหญ่เกินชั้นหน่วยความจำที่เร็วกว่า

ปัญหาที่เกิดขึ้น
ถ้า working set ใหญ่กว่า Cache โปรแกรมจะรอ RAM บ่อยขึ้น ถ้าใหญ่กว่า RAM และระบบพึ่ง virtual memory อาจเกิด paging หรือ swapping ไปยัง storage และถ้าเป็น Spark ข้อมูลกลางอาจ spill ลง disk แต่ละกรณีไม่เหมือนกัน แม้ภาพรวมจะคล้ายกันคือข้อมูลตกจากชั้นเร็วไปยังชั้นที่ช้ากว่า

คำว่า “swap” ในต้นฉบับใช้เพื่อสร้างภาพว่าข้อมูลถูกย้ายเข้าออก disk ซ้ำ ๆ ในเครื่องสมัยใหม่ควรใช้คำให้ตรงกรณี: cache miss, page fault, paging, application-level spill และ reread from storage มีผู้ควบคุมและรูปแบบต้นทุนต่างกัน การวิเคราะห์ที่ดีต้องดู metric ให้รู้ว่าเกิดแบบใดจริง

แบบจำลองเวลาอย่างง่าย
T ≈ T_compute + N_cacheMiss × L_memory
     + Bytes_spill / BW_storage + T_seek/request
     + T_communication + T_synchronization

แบบจำลองนี้ตั้งใจให้เห็นองค์ประกอบ ไม่ใช่สูตรทำนายเวลาที่แม่นทุกเครื่อง ค่า latency และ bandwidth ซ้อนทับกันได้ ระบบมี prefetch, out-of-order execution และ asynchronous I/O แต่ถ้าส่วนหนึ่งใหญ่กว่าส่วนอื่นมาก การกำจัดส่วนนั้นย่อมเปลี่ยนเวลาได้มากกว่าการเพิ่มความเร็วของ arithmetic เล็กน้อย

Memory Hierarchy เป็นบันได ไม่ใช่สวิตช์สองสถานะ

ระดับขนาดโดยทั่วไปลักษณะเมื่อข้อมูลไม่พอ
Registerเล็กมากใกล้หน่วยคำนวณที่สุดต้องอ่าน Cache
L1/L2/L3 CacheKB ถึงหลาย MBเร็วและแบ่งปันต่างระดับตามสถาปัตยกรรมเกิด cache miss ไป RAM
RAMGB ถึง TBใหญ่กว่าแต่ช้ากว่า Cacheอาจ paging หรือ application spill
Local SSD/HDDTBbandwidth/latency ต่างจาก RAM มากอาจอ่าน remote storage
Network/Object Storageขยายได้มากมี network และ request overheadคอขวดอาจย้ายไปเครือข่าย

Superlinear อาจเกิดเมื่อ partition ข้ามขอบระดับใดระดับหนึ่ง ไม่จำเป็นต้องเป็น RAM → Disk เท่านั้น การแบ่งให้ state พอดี L3 Cache ก็สร้างผลมากได้ โดยเฉพาะอัลกอริทึม Dynamic Programming ที่อ่านค่าข้างเคียงซ้ำ ๆ

ทางแก้แรก: จัดลำดับการเข้าถึงให้ swap น้อยลง

ถ้าแบ่ง y เป็น block ที่พอดีกับระดับหน่วยความจำเป้าหมาย แล้วประมวลผลทุกค่า x ที่เกี่ยวข้องให้ครบก่อนเปลี่ยน block เราเพิ่ม data reuse และลดการโหลดซ้ำ เทคนิคนี้เรียกได้ทั้ง blocking หรือ tiling ตามบริบท

จำนวนคำนวณยัง ≈ n × m
แต่จำนวนครั้งที่โหลด block ซ้ำลดลง

เราไม่ควรสรุปว่าจะเหลือ swap “หนึ่งครั้ง” เสมอ เพราะขึ้นกับขนาด block, replacement policy, output และข้อมูลอื่นที่แย่ง Cache ประเด็นที่ถูกคือการจัดลำดับใหม่ลด I/O หรือ miss จากการใช้ข้อมูลซ้ำให้คุ้มก่อนถูกแทนที่

Loop Interchange และ Tiling

for (jb = 0; jb < m; jb += B)
  for (i = 0; i < n; i++)
    for (j = jb; j < min(jb+B, m); j++)
      z[i][j] = functionof(x[i], y[j]);

ตัวอย่างนี้สื่อแนวคิด แต่ loop order ที่ดีที่สุดขึ้นกับ layout ของ z, ขนาดของ x, การเขียน output และ vectorization หากแก้ให้ y reuse ดีขึ้นแต่เขียน z แบบกระโดด เราอาจย้ายปัญหาจาก input ไป output จึงควร profile ทั้งระบบ ไม่เลือกจาก intuition เพียงอย่างเดียว

ก่อนเพิ่มเครื่อง ลองลดงานที่ไม่จำเป็นบนเครื่องเดียว

ถ้าการทำ tiling, compression, sparse representation หรือเปลี่ยน data layout ทำให้ sequential baseline เร็วขึ้น 10 เท่า ตัวเลข speedup ของ parallel version อาจลดลง แต่ผลงานจริงดีขึ้น นี่เป็น paradox ที่สำคัญ: งานวิจัยที่รายงาน speedup ต่ำกว่าอาจมีโปรแกรมเร็วกว่าจริง เพราะ baseline ได้รับการ optimize อย่างยุติธรรมกว่า

5เมื่อแบ่งงานแล้ว Working Set ข้ามหน้าผาของ Memory Hierarchy

แนวคิดหลัก
แบ่ง y ออกเป็น p ส่วน แล้วให้แต่ละหน่วยประมวลผลรับผิดชอบส่วนของตน หากส่วนย่อยพอดีกับ Cache หรือ RAM ที่เข้าถึงได้เร็วขึ้น เราได้ประโยชน์สองชั้น: งานคำนวณต่อหน่วยลดลง และเวลาเสียไปกับการเคลื่อนข้อมูลต่อ operation ลดลง
// worker 1                              // worker 2 (ทำพร้อมกัน)
for (i=0; i<n; i++)                       for (i=0; i<n; i++)
  for (j=0; j<memory_size; j++)             for (j=memory_size+1; j<n; j++)
    z[i][j] = functionof(x[i],y[j]);          z[i][j] = functionof(x[i],y[j]);
แบบจำลองเชิงแนวคิด
T₁ = W × c_slow + H₁
Tₚ ≈ (W/p) × c_fast + Hₚ
เมื่อ c_fast << c_slow, Sₚ อาจมากกว่า p
นี่คือจุดที่ Superlinear เกิดขึ้นได้
ถ้าใช้ 2 worker เราคาดจากการหารงานอย่างเดียวว่าจะเร็วขึ้นประมาณ 2 เท่า แต่ถ้าแต่ละ worker เปลี่ยนจากการอ่าน Disk มาอ่าน RAM หรือจาก RAM มาใช้ Cache ได้มากขึ้น ค่าใช้จ่ายต่อ operation ก็ลดลงด้วย Speedup จึงเป็นผลคูณโดยคร่าว ๆ ของ “งานต่อ worker ลดลง” กับ “แต่ละ operation ถูกลง” หลังหัก communication และ coordination overhead

CPU 8 ตัวบนเครื่องเดียว ไม่เท่ากับ 8 เครื่องเสมอไป

คำว่า CPU ในเอกสารเก่าอาจหมายถึง processor จริงหลายตัว ขณะที่เครื่องปัจจุบันมี socket, core, hardware thread และ cache ที่แบ่งปันกันต่างระดับ หาก 8 cores แชร์ memory bandwidth หรือ L3 Cache เดียวกัน การแบ่งงานไม่ได้เพิ่มทรัพยากรทุกชนิดแปดเท่า ในทางกลับกันหลาย socket อาจมี memory controller แยกกันและให้ aggregate bandwidth สูงขึ้น

เพิ่มอะไรสิ่งที่อาจเพิ่มตามสิ่งที่อาจยังแชร์กัน
Hardware threadความสามารถซ่อน latency บางส่วนexecution units, cache และ bandwidth
Coreหน่วยคำนวณและ L1/L2 บางส่วนL3, memory channels หรือ power budget
Socketcores, cache และ memory channelsinterconnect และ access ข้าม NUMA node
เครื่องในคลัสเตอร์CPU, RAM, local disk, bandwidth ต่อเครื่องnetwork fabric และ storage backend

Aggregate Memory Capacity และ Aggregate Bandwidth

การใช้หลายเครื่องเพิ่มทั้งความจุ RAM รวมและ bandwidth รวม หาก single-node baseline ถูกจำกัดด้วย memory bandwidth การกระจายไปหลาย node อาจเพิ่มช่องทางอ่านข้อมูลพร้อมกัน แม้แต่ละ node ไม่ได้ประมวลผลเร็วกว่าเดิม นี่เป็นอีกสาเหตุหนึ่งที่ Speedup อาจสูงกว่าการนับ core อย่างผิวเผิน

NUMA: Memory ไม่ได้อยู่ห่างจากทุก Core เท่ากัน

บนเครื่องหลาย socket หน่วยความจำที่ผูกกับ socket เดียวกันเข้าถึงได้เร็วกว่า remote NUMA node หาก thread ถูกย้ายข้าม socket หรือข้อมูลถูก allocate คนละ node อาจเสีย locality การ pin thread และ first-touch placement ช่วยได้ในบาง workload แต่ต้องวัดจริง การเพิ่ม core โดยไม่สน NUMA อาจทำให้ performance แย่ลงแทนที่จะดีขึ้น

False Sharing: แบ่งตัวแปรแล้วแต่ยังแย่ง Cache Line เดียวกัน

สอง thread อาจเขียน counter คนละตัว แต่ counter อยู่ใน cache line เดียวกัน Cache coherence จึงส่ง line กลับไปกลับมาระหว่าง cores เหมือนคนสองคนเขียนคนละช่องบนกระดาษแผ่นเดียวและต้องส่งทั้งแผ่นทุกครั้ง ปัญหานี้เรียก false sharing การ padding หรือจัด layout ใหม่อาจลด overhead ได้

Cache Capacity ไม่ใช่เหตุผลเดียว

Superlinear มักเกิดเป็นช่วง ไม่ได้โตต่อไปตลอด

ลองเพิ่ม p จาก 1, 2, 4, 8, 16 เครื่อง ช่วง 1→4 อาจยัง sublinear เพราะทุก partition ใหญ่เกิน RAM พอถึง 8 partition จึงพอดีและเวลาไหลลงแรง เกิด superlinear เทียบ baseline แต่เมื่อเพิ่มเป็น 16 เครื่อง ข้อมูลพอดีอยู่แล้ว ประโยชน์ใหม่เหลือแค่แบ่งงาน ขณะที่ communication เพิ่ม ผลตอบแทนจึงลดลง

ภาพที่ควรวาดเวลาอธิบาย
แกน x เป็นจำนวน worker และแกน y เป็นเวลา กราฟไม่จำเป็นต้องลดอย่างเรียบ อาจมี “หัวเข่า” ตรงจุดที่ working set ต่อ worker พอดี RAM หรือ Cache ก่อนจุดนั้นระบบติด I/O หลังจุดนั้นคอขวดย้ายไป network หรือ synchronization การ tune ที่ดีคือหาว่าหัวเข่าอยู่ตรงไหน ไม่ใช่เพิ่มเครื่องจนงบหมด

6Superlinear เกิดได้จากอะไรบ้าง

Cache effect เป็นคำอธิบายที่สำคัญ แต่ไม่ใช่คำอธิบายเดียว เราควรจัดสาเหตุเป็นหมวด เพราะแต่ละหมวดพิสูจน์และทำซ้ำต่างกัน

หมวดกลไกหลักฐานที่ควรหา
Memory hierarchyworking set เล็กลงจน miss, paging หรือ spill ลดcache misses, page faults, spill bytes, memory bandwidth
Aggregate resourcesหลาย node เพิ่ม RAM, cache, disk และ bandwidth รวมresource utilization และ bandwidth ต่อ node
Algorithmic changeparallel version ลดจำนวนงานหรือใช้ decomposition ที่ดีกว่าจำนวน states/iterations/operations ที่ทำจริง
Search anomalyworker บางตัวพบคำตอบหรือ bound เร็ว ทำให้ตัด search spaceจำนวน node ที่สำรวจและเวลาที่พบ incumbent
Measurement artifactbaseline เย็นกว่า ขอบเวลาต่าง หรือใช้ configuration ไม่เท่ากันexperimental protocol และ repeated runs

1) Cache, Paging และ Spill ลดลง

เป็นกรณีหลักของบทนี้ การแบ่งข้อมูลทำให้ state ต่อ worker เล็กลงและ reuse ในหน่วยความจำเร็วขึ้น สัญญาณที่คาดว่าจะเห็นคือ cache miss rate ลด, major page fault ลด, disk read/write ลด หรือ Spark spill metrics ลด หากเวลาลดแต่ metrics เหล่านี้ไม่เปลี่ยน ควรมองหาสาเหตุอื่น

2) รวมทรัพยากรหลายชนิด ไม่ใช่เพิ่ม CPU อย่างเดียว

เมื่อเพิ่ม node เราเพิ่ม RAM, cache, local SSD และ memory bandwidth ไปด้วย การกล่าวว่า “ใช้ CPU 8 ตัว” อาจซ่อนว่าระบบแบบขนานมี RAM รวมแปดเท่าและ disk channels หลายชุด หากต้องการรู้สาเหตุ ควรทดลองแยก เช่นใช้ core เท่าเดิมแต่จำกัด memory หรือเปรียบเทียบ scale-up กับ scale-out

3) Parallel Algorithm ทำงานน้อยกว่า Sequential Algorithm

บางครั้งโปรแกรมขนานไม่ได้เป็นเพียงเวอร์ชันแบ่งครึ่งของโปรแกรมลำดับ แต่ใช้โครงสร้างข้อมูล การประมาณ หรือ pruning ต่างกัน หากจำนวน operation ลดลง เราได้ algorithmic speedup ประกอบกับ parallel speedup การรายงานยังทำได้ แต่ควรบอกให้ชัดว่าเปรียบเทียบ implementation ต่างอัลกอริทึม ไม่ใช่สรุปว่า hardware อย่างเดียวสร้างผลทั้งหมด

4) Search Anomaly และ Branch-and-Bound

ในปัญหา search, optimization หรือ game tree worker หลายตัวสำรวจคนละส่วน บางตัวอาจพบคำตอบดีเร็ว แล้วส่ง bound ไปตัดกิ่งขนาดใหญ่ที่ sequential order เดิมจะยังไม่พบ ผลคือ parallel run สำรวจ node น้อยกว่าอย่างมาก แต่ใน run อื่นอาจพบ bound ช้าและไม่ได้ผลเช่นนั้น Superlinear แบบนี้จึงขึ้นกับ search order และข้อมูล

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

5) Randomized Algorithm

เมื่อ worker ใช้ random seed ต่างกัน โอกาสที่อย่างน้อยหนึ่งตัวพบเส้นทางดีเร็วเพิ่มขึ้น ลักษณะนี้คล้าย portfolio search Speedup อาจ superlinear ในค่าเฉลี่ยบาง distribution แต่ต้องรายงานหลาย run, seed และสถิติการกระจาย ไม่ควรเลือกเฉพาะรอบที่ดีที่สุด

6) Baseline ไม่ดีหรือวัดไม่เท่ากัน

Superlinear ที่เกิดจาก baseline ไม่ได้ optimize ไม่ใช่หลักฐานว่าปรากฏการณ์ไม่มีจริง แต่ไม่ใช่คำตอบที่น่าภูมิใจเท่าใดนัก ถ้า sequential code ใช้ compiler flag ต่างกัน ไม่ทำ tiling หรือใช้ algorithm คนละคุณภาพ เรากำลังวัดช่องว่างของ implementation มากกว่าประโยชน์ของ parallelism

ข้อควรระวัง: ไม่ใช่ทุกกรณีจะเกิด Superlinear
แม้แบ่งแล้ว working set เล็กลง แต่ overhead จากการส่งข้อมูล Synchronization, NUMA หรือ startup อาจมากกว่าประโยชน์ และถ้าทุก partition ยังใหญ่เกิน resource ระดับเดิม คอขวดอาจไม่เปลี่ยน การแบ่ง “Big” เป็น “Big หลายก้อน” ไม่ได้ทำให้ปัญหาเล็กพอโดยอัตโนมัติ
สถานการณ์ผลลัพธ์
ข้อมูลพอดีระดับเร็วอยู่แล้วไม่มี memory cliff ให้ข้าม Speedup มักเป็น linear หรือต่ำกว่าเพราะ overhead
ข้อมูลใหญ่กว่า Cache/RAM เล็กน้อย แบ่งแล้วพอดีมีโอกาสเกิด Superlinear speedup จาก miss, paging หรือ spill ที่ลดลงมาก
แบ่งแล้วยังไม่พอดีระดับเดิมคอขวดยังอยู่ อาจดีขึ้นตามสัดส่วนแต่ไม่จำเป็นต้อง superlinear
Partition เล็กเกินไปScheduling, communication และ metadata overhead ครอบงำ
Key หรือ workload เอียงworker ส่วนใหญ่เสร็จเร็วแต่ยังรอ straggler

Amdahl’s Law ไม่ได้ถูกทำลาย แต่สมมุติฐานถูกเปลี่ยน

Amdahl’s Law มองสัดส่วนงานลำดับและงานขนานภายใต้แบบจำลองที่ workload คงเดิม หาก parallel execution เปลี่ยน cache behavior หรือจำนวนงานจริง เวลาต่อหน่วยงานไม่เหมือน baseline แล้ว การใช้สูตรง่าย ๆ เพื่อประกาศว่า “เป็นไปไม่ได้” จึงใช้แบบจำลองผิดขอบเขต

Amdahl’s Law แบบพื้นฐาน
Sₚ = 1 / (f + (1-f)/p)
เมื่อ f คือสัดส่วนที่ไม่ขนาน และต้นทุนงานอื่นคงเดิม

ถ้า (f=0) สูตรให้ (S_p=p) แต่ Cache effect ทำให้ต้นทุนของส่วน ((1-f)) ต่อ operation ลดลงด้วย เราจึงต้องเพิ่มพารามิเตอร์ด้าน memory หรือใช้แบบจำลองที่ละเอียดกว่า ไม่ใช่บังคับข้อมูลให้เข้ากับสูตรที่ละตัวแปรสำคัญออกไป

Gustafson’s Law ตอบอีกคำถามหนึ่ง

Amdahl ถามว่างานขนาดคงเดิมเร็วขึ้นเพียงใด ส่วน Gustafson มองว่าเมื่อมีทรัพยากรมากขึ้น เรามักแก้ปัญหาใหญ่ขึ้นภายในเวลาที่รับได้ จึงเหมาะกับ weak scaling มากกว่า แต่ Gustafson ไม่ใช่คำอธิบาย Superlinear โดยตรง เราไม่ควรนำกฎสองข้อมาแข่งขันว่าใคร “ถูกกว่า” เพราะตอบคนละคำถาม

7เชื่อมโยงกับ Big Data และ Spark

ความเกี่ยวข้องโดยตรง
เมื่อ Spark กระจาย partition ไปยัง executor หลายตัว การเพิ่มทรัพยากรอาจลดขนาด working set ต่อ task ลด spill และเพิ่ม aggregate memory bandwidth แต่คำว่า “partition พอดี executor memory” ยังหยาบเกินไป เพราะ executor อาจรันหลาย tasks พร้อมกัน และ memory ยังถูกแบ่งระหว่าง execution, storage, object overhead และระบบ JVM

Dataset 100 GB กับ RAM 64 GB ไม่ได้บอกว่าจะ Spill แน่นอน

ต้นฉบับยกตัวอย่าง dataset 100 GB บน RAM 64 GB แล้วสรุปว่าต้อง spill คำอธิบายนี้ง่ายเกินจริง เพราะงาน scan/filter สามารถ stream ทีละ partition โดยไม่ต้องเก็บ dataset ทั้งก้อนใน RAM งานจะ spill เมื่อ operator ต้องรักษาสถานะ เช่น sort, hash aggregation, join หรือ cache และ state นั้นเกิน memory ที่มี

เปรียบเหมือนน้ำผ่านท่อ
น้ำหนึ่งล้านลิตรผ่านท่อที่จุได้สิบลิตร ไม่ได้แปลว่าต้องสร้างถังหนึ่งล้านลิตร หากเราปล่อยน้ำไหลผ่านต่อเนื่อง แต่ถ้าต้องแยกน้ำตามสีแล้วรอรวมทุกสี เราต้องมีพื้นที่พัก Dataset ใหญ่กว่า RAM จึงไม่ใช่เหตุผลพอที่จะทำนาย spill ต้องดูชนิดของ operation และ state ที่ต้องถือ

Partition เล็กลงช่วยอะไร

แต่ partition เล็กเกินไปทำให้มี task จำนวนมาก เพิ่ม scheduling overhead, task startup, metadata, network connection และไฟล์เล็ก ผลลัพธ์จึงมีช่วงที่เหมาะ ไม่ใช่ “ยิ่งเล็กยิ่งดี”

จำนวน Partition ไม่เท่ากับจำนวน Core

มักมี partition มากกว่า core เพื่อให้ load balancing ทำงานได้ หากมี 8 cores และ 8 partitions แล้ว partition หนึ่งหนักกว่าที่เหลือสิบเท่า เจ็ด core จะว่างรอตัวสุดท้าย หากมี partitions มากขึ้น scheduler มีโอกาสเฉลี่ยความไม่แน่นอนได้ดีขึ้น แต่ถ้า key เดียวหนักมาก การเพิ่ม partition ทั่วไปอาจไม่แก้ เพราะ key เดียวกันยังไป partition เดียว

Spill ใน Spark เกิดที่ Operator ใด

Operator/สถานการณ์State ที่ต้องถือสิ่งที่ควรดู
Sortbuffer ของ record และ runsspill bytes, peak memory, input size
Hash aggregateตาราง key → aggregate stateจำนวน key ไม่ซ้ำและ skew
Joinhash table หรือ sorted partitionsขนาดแต่ละฝั่งและ join strategy
Shufflemap output buffers และ reduce fetch/mergeshuffle read/write, fetch wait, records spilled
Cache/Persistdataset partitions ที่เก็บใช้ซ้ำstorage level, eviction และ recomputation

reduceByKey ช่วยก่อนจะต้องเพิ่มเครื่อง

ถ้างานเป็นการรวมตาม key การใช้ reduceByKey หรือ partial aggregation ลด record ก่อน Shuffle ได้มากกว่า groupByKey ในหลายกรณี นี่เป็นการลด working set ด้วย Algebra ไม่ใช่ hardware การเลือก representation และ operator ที่ดีอาจกำจัด spill ได้โดยไม่เพิ่ม node

Broadcast Join เปลี่ยนรูปการเคลื่อนข้อมูล

หากตารางหนึ่งเล็กพอ broadcast ไปทุก executor ได้ เราหลีกเลี่ยงการ Shuffle ตารางใหญ่ทั้งสองฝั่ง แต่ broadcast table ต้องพอดี memory ของ executor และถูกทำสำเนาหลายชุด หากประเมินขนาดผิด เราอาจเปลี่ยนจาก network bottleneck ไปเป็น memory pressure การตัดสินใจจึงต้องอาศัยสถิติและ physical plan

Cache มีประโยชน์เมื่อใช้ซ้ำ ไม่ใช่เพราะ RAM ว่าง

Dataset ที่ใช้ครั้งเดียวไม่จำเป็นต้อง cache การเก็บอาจเพิ่ม serialization และ eviction หากใช้ซ้ำหลาย iterations เช่น machine learning หรือ graph algorithm การ cache working set ที่เหมาะช่วยตัดการอ่านและคำนวณ lineage ซ้ำ จุดนี้เชื่อมกับบทก่อน: Immutability ทำให้ cached partition ยังอ้างข้อมูลรุ่นเดิมได้ และ Lazy Evaluation ทำให้ cache ยังไม่เกิดจน action เรียกใช้

Dynamic Allocation ทำให้การทดลองซับซ้อนขึ้น

หากจำนวน executor เปลี่ยนระหว่าง run การระบุว่า “ใช้ p processors” อาจไม่ตรงตลอดเวลา การวัด scaling ควรควบคุมจำนวน executor, cores, memory และ concurrent jobs ให้ชัด หรืออย่างน้อยรายงาน executor timeline และ resource-seconds ไม่เช่นนั้น Speedup อาจมาจากทรัพยากรจริงที่ต่างจากค่าที่ตั้งใจ

Cloud VM มีความแปรปรวน

เครื่องเสมือนอาจแชร์ host, network และ storage กับผู้ใช้อื่น มี warm cache, burst credit หรือ throttling การทดลองครั้งเดียวจึงไม่น่าเชื่อถือ ควรรันซ้ำ สุ่มลำดับ configuration และรายงาน median พร้อม percentile หรือช่วงความเชื่อมั่นตามความเหมาะสม

อย่า Tune จากสูตร “กี่ MB ต่อ Partition” สูตรเดียว
ขนาด byte เท่ากันแต่จำนวน object, จำนวน key, compression ratio และ operation ต่างกัน ใช้ memory ไม่เท่ากัน การ tune ควรเริ่มจาก metrics ของ stage ที่ช้า ดู spill, GC, skew และ task distribution แล้วเปลี่ยนทีละปัจจัย

8วิธีพิสูจน์ว่า Superlinear ที่เห็นมาจากอะไร

คำอธิบายเรื่อง Cache ฟังสมเหตุสมผล แต่ความสมเหตุสมผลยังไม่ใช่หลักฐาน เราต้องออกแบบการทดลองที่ทำให้สาเหตุคู่แข่งค่อย ๆ ถูกตัดออก

ขั้นที่ 1: กำหนด Baseline ให้ยุติธรรม

ขั้นที่ 2: วัดหลายขนาดและหลายจำนวน Worker

อย่าวัดเพียง p=1 กับ p=8 ให้ใช้ 1, 2, 4, 8, 16 หากทำได้ แล้ววาด runtime, speedup, efficiency และ cost จุดที่กราฟเปลี่ยนความชันอาจตรงกับ working set ที่พอดี Cache/RAM หากมีเพียงสองจุด เราไม่เห็นหัวเข่าและอธิบายกลไกยาก

ขั้นที่ 3: เก็บ Hardware Counters และ System Metrics

สมมุติฐานMetric ที่ช่วยสนับสนุน
Cache ดีขึ้นLLC misses, cache miss rate, cycles stalled on memory
Paging ลดลงmajor/minor page faults, swap I/O
Spill ลดลงmemory/disk spill bytes, temporary files
Bandwidth เพิ่มmemory bandwidth, disk throughput, network throughput
งานจริงลดลงinstruction count, iterations, states explored
Load สมดุลขึ้นtask duration distribution และเวลาของ straggler

ขั้นที่ 4: ทำ Controlled Experiment

หากสงสัย RAM capacity ให้จำกัด memory ของ sequential run หลายระดับ หากสงสัย Cache ให้เปลี่ยน problem size รอบขอบความจุ หรือใช้ blocking size หลายค่า หากสงสัย search order ให้ตรึง seed และนับ states ที่สำรวจ การเปลี่ยนทีละปัจจัยช่วยแยก correlation ออกจาก cause

ขั้นที่ 5: แยก Cold Run กับ Warm Run

Cold run อาจรวมการอ่านจาก storage และ JIT compilation ส่วน warm run ได้ประโยชน์จาก OS page cache, JVM JIT, connection pool และ cached data ทั้งสองแบบมีความหมาย แต่ต้องระบุและเทียบแบบเดียวกัน ควร warm up ตาม protocol แล้วรายงานอย่างโปร่งใส หรือรายงานทั้ง cold และ steady-state

ขั้นที่ 6: รันซ้ำและรายงาน Distribution

ค่าเฉลี่ยอย่างเดียวถูกลากโดย outlier ได้ ควรรายงานจำนวนรอบ median, min/max หรือ percentile พร้อมระบุเครื่องและ workload อื่นที่รันร่วมกัน สำหรับ randomized search ต้องรายงานหลาย seed ไม่เลือกเฉพาะ best case

ขั้นที่ 7: ตรวจคำตอบและปริมาณงาน

โปรแกรมที่เร็วกว่ามากอาจคำนวณไม่ครบ ใช้ early stopping คนละเกณฑ์ หรือผลตกหล่นเพราะ race condition จึงควรมี checksum, objective value, tolerance และ count ของ states/records ที่ประมวลผล ตัวเลข performance ที่ไม่มี correctness ไม่ใช่ Speedup แต่เป็นโปรแกรมที่เลิกงานก่อน

หลักการง่าย ๆ
ถ้าอ้างว่า Superlinear เพราะ Cache ลด ควรมีตัวเลข Cache สนับสนุน ถ้าอ้างว่าเพราะ Spill หาย ควรแสดง Spill metrics ถ้ามีเพียง runtime สองค่า เราพิสูจน์ได้เพียงว่า “รอบนี้เร็วขึ้น” ยังพิสูจน์ไม่ได้ว่าทำไม

9แล็บย่อ: ตามหาหัวเข่าของ Memory Hierarchy

แล็บนี้ไม่ต้องใช้คลัสเตอร์ใหญ่ก็ทำได้ เป้าหมายไม่ใช่สร้างสถิติโลก แต่ฝึกเชื่อมกราฟเวลารันกับ working set และ Cache

แนวทางการทดลอง

  1. สร้าง array หลายขนาด ตั้งแต่เล็กกว่า Cache ไปจนใหญ่กว่า RAM ส่วนที่จัดสรรได้อย่างปลอดภัย
  2. เขียน kernel ที่อ่านซ้ำแบบต่อเนื่อง และอีกแบบที่เข้าถึงตำแหน่งสุ่ม
  3. วัด throughput หลายรอบ แยก warm-up และเก็บ median
  4. ทดลอง blocking size หลายค่าโดยไม่เพิ่ม thread
  5. เพิ่ม thread 1, 2, 4, 8 และดูว่าจุดเปลี่ยนสัมพันธ์กับ working set ต่อ thread หรือไม่
  6. ถ้ามีเครื่องมือ ให้เก็บ cache-miss และ page-fault counters

ตารางบันทึกที่ควรมี

WorkersProblem SizeBlock SizeTimeSpeedupCache MissSpill/Page Fault
1.........1.00......
2..................

สิ่งที่ต้องอภิปราย

10สรุปและขั้นตอนถัดไป

Superlinear Speedup ไม่ใช่เรื่องเหนือธรรมชาติ และไม่ใช่สิ่งที่ควรเกิดทุกครั้ง มันเป็นสัญญาณว่าการเพิ่ม parallelism เปลี่ยนปัจจัยอื่นไปพร้อมกัน—working set พอดี Cache/RAM, spill ลด, bandwidth รวมเพิ่ม, search order เปลี่ยน หรือจำนวนงานจริงลดลง

กรณีปี 2002 ที่ลดเวลาจาก 7.5 วันเหลือ 8 ชั่วโมงบน CPU 8 ตัวมีคุณค่ามากกว่าตัวเลข 22.5 เท่า เพราะสอนให้มองข้ามจำนวน CPU ไปยังเส้นทางของข้อมูล และเมื่อแนวคิดเดียวกันถูกนักศึกษานำไปใช้กับ Dynamic Programming บน Spark ในอีกยี่สิบปีต่อมา สิ่งที่สืบทอดไม่ใช่ platform แต่คือวิธีคิด

สิ่งที่ควรจำจากบทนี้มีดังนี้:

บทถัดไปจะเปลี่ยนจากคำถามว่า “ทำอย่างไรให้เร็วขึ้น” ไปสู่คำถามว่า “เมื่อข้อมูลอยู่หลายเครื่อง เราจะให้แต่ละสำเนาเห็นความจริงแบบใด” ผ่าน CAP, ACID, BASE และ NoSQL เป็นการย้ายจากข้อจำกัดของเวลาและหน่วยความจำ ไปสู่ข้อจำกัดของความสอดคล้องและความล้มเหลวของเครือข่าย

คำถามซ้อมสอบ
  1. คำนวณ Speedup, Efficiency และ Cost จากกรณีใช้ CPU 8 ตัว ลดเวลาจาก 7.5 วันเหลือ 8 ชั่วโมง แล้วอธิบายว่า Efficiency เกิน 100% หมายถึงอะไร
  2. อธิบายความแตกต่างระหว่าง Cache Miss, Paging/Swapping และ Spark Spill พร้อมยก metric ที่ช่วยแยกแต่ละกรณี
  3. อธิบาย Temporal Locality และ Spatial Locality จาก loop ที่คำนวณทุกคู่ของ x[i] และ y[j]
  4. แสดงว่า Tiling เปลี่ยนจำนวนครั้งที่เคลื่อนข้อมูลได้อย่างไร แม้จำนวน operation ทางคณิตศาสตร์ยังเป็น O(nm)
  5. อธิบายว่าทำไม Superlinear Speedup ไม่ได้ทำลาย Amdahl’s Law แต่กำลังเปลี่ยนสมมุติฐานเรื่องต้นทุนต่อ operation หรือจำนวนงานจริง
  6. เปรียบเทียบ Superlinear จาก Cache Effect กับ Superlinear จาก Search Anomaly ว่าควรใช้หลักฐานคนละชนิดอย่างไร
  7. อธิบายว่าทำไม dataset ขนาด 100 GB บนเครื่อง RAM 64 GB จึงไม่ได้แปลว่า Spark ต้อง spill เสมอไป
  8. วิเคราะห์ผลดีและผลเสียของการลดขนาด partition ใน Spark รวมถึงกรณีที่ key skew ทำให้เพิ่ม partition แล้วยังไม่หายช้า
  9. ออกแบบการทดลอง p = 1, 2, 4, 8, 16 เพื่อพิสูจน์ว่า Speedup ที่พบเกิดจาก Cache หรือ Memory Capacity โดยระบุ metric และวิธีควบคุมตัวแปร
  10. อธิบายประโยคว่า “Speedup ลดลง แต่โปรแกรมดีขึ้น” ผ่านกรณีที่ sequential baseline ได้รับการ optimize ด้วย Tiling