ระบบเชิงเส้นที่เร็วที่สุดแก้ปัญหาสำหรับเมทริกซ์จตุรัสขนาดเล็ก (10x10)


9

ฉันสนใจมากในการปรับนรกให้เหมาะสมกับการแก้ไขระบบเชิงเส้นสำหรับเมทริกซ์ขนาดเล็ก (10x10) บางครั้งเรียกว่าเมทริกซ์จิ๋ว มีวิธีแก้ปัญหาพร้อมสำหรับเรื่องนี้หรือไม่? เมทริกซ์สามารถอนุมานได้ว่าไม่มีความหมาย

ตัวแก้ปัญหานี้จะต้องดำเนินการเกิน 1,000 000 ครั้งในหน่วยไมโครวินาทีบน Intel CPU ฉันกำลังพูดถึงระดับการเพิ่มประสิทธิภาพที่ใช้ในเกมคอมพิวเตอร์ ไม่ว่าฉันจะเขียนโค้ดในแอสเซมบลีและสถาปัตยกรรมเฉพาะหรือศึกษาความแม่นยำหรือความน่าเชื่อถือในการลดการแลกเปลี่ยนและใช้แฮ็กจุดลอยตัว (ฉันใช้แฟล็ก -ffast-math คอมไพล์ไม่มีปัญหา) การแก้ปัญหาอาจล้มเหลวได้ประมาณ 20% ของเวลา!

partialPivLu ของ Eigen นั้นเร็วที่สุดในเบนช์มาร์กปัจจุบันของฉันมีประสิทธิภาพเหนือกว่า LAPACK เมื่อปรับให้เหมาะสมกับ -O3 และคอมไพเลอร์ที่ดี แต่ตอนนี้ฉันอยู่ที่จุดของการแก้ปัญหาเชิงเส้นกำหนดเอง คำแนะนำใด ๆ ที่จะได้รับการชื่นชมอย่างมาก ฉันจะทำให้โซลูชันของฉันเป็นโอเพ่นซอร์สและฉันจะทำมุมมองเชิงลึกที่สำคัญในสิ่งพิมพ์ ฯลฯ

ที่เกี่ยวข้อง: ความเร็วในการแก้ระบบเชิงเส้นด้วยเมทริกซ์บล็อกแนวทแยง วิธีที่เร็วที่สุดในการแปลงเมทริกซ์นับล้านคืออะไร? https://stackoverflow.com/q/50909385/1489510


7
ดูเหมือนว่าเป้าหมายยืดเยื้อ สมมติว่าเราใช้ Skylake-X Xeon Platinum 8180 ที่เร็วที่สุดพร้อมกับทฤษฏีทรูพล็อตสูงสุดของ TFLOP ที่มีความแม่นยำเดียว 4 ระบบและระบบ 10x10 หนึ่งระบบต้องการประมาณ 700 (ประมาณ 2n ** 3/3) การแก้ไขจุดลอยตัว จากนั้นระบบดังกล่าวจำนวน 1 ล้านชุดสามารถแก้ไขได้ในทางทฤษฎีใน 175 ไมโครวินาที นั่นคือตัวเลขความเร็วแสงไม่สามารถเกินได้ คุณสามารถแบ่งปันประสิทธิภาพที่คุณประสบความสำเร็จกับรหัสที่มีอยู่เร็วที่สุดของคุณได้หรือไม่? BTW เป็นข้อมูลความแม่นยำเดียวหรือความแม่นยำสองเท่า?
njuffa

@njuffa ใช่ฉันมุ่งมั่นที่จะบรรลุใกล้ 1ms แต่ไมโครเป็นอีกเรื่องหนึ่ง สำหรับไมโครฉันถือว่าการใช้ประโยชน์จากโครงสร้างผกผันที่เพิ่มขึ้นในชุดโดยการตรวจสอบเมทริกซ์ที่คล้ายกันซึ่งเกิดขึ้นบ่อยครั้ง ความสมบูรณ์แบบอยู่ในช่วง 10-500ms ขึ้นอยู่กับโปรเซสเซอร์ ความแม่นยำเป็นสองเท่าหรือสองเท่าที่ซับซ้อน ความแม่นยำเดี่ยวจะช้าลง
rfabbri

@njuffa ฉันสามารถลดหรือเพิ่มความแม่นยำสำหรับความเร็วได้
rfabbri

2
ดูเหมือนว่าความแม่นยำ / ความแม่นยำไม่ใช่สิ่งที่คุณให้ความสำคัญ สำหรับเป้าหมายของคุณวิธีการวนซ้ำอาจถูกตัดทอนด้วยจำนวนการประเมินค่อนข้างน้อยมีประโยชน์หรือไม่ โดยเฉพาะถ้าคุณมีการเดาเริ่มต้นที่สมเหตุสมผล
Spencer Bryngelson

1
คุณหมุนหรือไม่ คุณช่วย QR การแยกตัวประกอบแทนการกำจัดแบบเกาส์เซียนได้ไหม คุณ interleave ระบบของคุณเพื่อให้คุณสามารถใช้คำสั่ง SIMD และทำหลายระบบพร้อมกันหรือไม่? คุณเขียนโปรแกรมแบบเส้นตรงโดยไม่มีการวนซ้ำและไม่มีการระบุที่อยู่ทางอ้อมหรือไม่? คุณต้องการความแม่นยำอะไรและระบบของคุณจะปรับอากาศอย่างไร พวกเขามีโครงสร้างที่สามารถใช้ประโยชน์ได้หรือไม่
Carl Christian

คำตอบ:


7

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

หากคุณตัดสินใจที่จะยกเลิกการใช้งานของคุณเองคุณอาจพบว่า PETSc ทำอะไรเพื่อให้บล็อกรูปแบบ CSR เป็นตัวอย่างที่มีประโยชน์แม้ว่า PETSc อาจเป็นเครื่องมือที่เหมาะสมสำหรับสิ่งที่คุณมีอยู่ในใจ แทนที่จะเขียนลูปพวกมันเขียนทุกการดำเนินการสำหรับเมทริกซ์เวกเตอร์ขนาดเล็กจำนวนมากอย่างชัดเจน (ดูไฟล์นี้ในที่เก็บของพวกเขา) สิ่งนี้รับประกันได้ว่าจะไม่มีคำสั่งสาขาเช่นคุณอาจได้รับกับวง เวอร์ชันของรหัสพร้อมคำแนะนำ AVX เป็นตัวอย่างที่ดีของวิธีใช้ส่วนขยายเวกเตอร์ ตัวอย่างเช่นฟังก์ชั่นนี้ใช้__m256dชนิดข้อมูลเพื่อใช้งานพร้อมกันในสี่คู่ในเวลาเดียวกัน คุณสามารถเพิ่มประสิทธิภาพที่เห็นได้ชัดเจนโดยเขียนการดำเนินการทั้งหมดโดยใช้ส่วนขยายแบบเวกเตอร์เฉพาะสำหรับการแยกตัวประกอบ LU แทนเมทริกซ์ - เวกเตอร์คูณ แทนที่จะเขียนรหัส C ด้วยมือคุณควรใช้สคริปต์เพื่อสร้างมันขึ้นมา มันอาจสนุกถ้าดูว่ามีความแตกต่างด้านประสิทธิภาพที่เห็นได้หรือไม่เมื่อคุณสั่งซื้อการปฏิบัติการบางส่วนใหม่เพื่อใช้ประโยชน์จากการส่งคำสั่ง

คุณอาจได้รับไมล์สะสมจากเครื่องมือSTOKEซึ่งจะสุ่มสำรวจพื้นที่ของการแปลงโปรแกรมที่เป็นไปได้เพื่อหาเวอร์ชั่นที่เร็วกว่า


เท็กซัส ฉันใช้ Eigen เช่น Map <const Matrix <complex, 10, 10>> AA (A) เรียบร้อยแล้ว จะตรวจสอบสิ่งอื่น ๆ
rfabbri

Eigen ยังมี AVX และแม้แต่ส่วนหัว complex.h สำหรับมัน ทำไมต้องเป็น PETSc มันยากที่จะแข่งขันกับ Eigen ในกรณีนี้ ฉันมีความเชี่ยวชาญ Eigen มากยิ่งขึ้นสำหรับปัญหาของฉันและด้วยกลยุทธ์เดือยที่ประมาณว่าแทนที่จะใช้จำนวนสูงสุดบนคอลัมน์ให้สลับเดือยทันทีเมื่อพบอีกอันที่มีขนาดใหญ่กว่า 3 อันดับ
rfabbri

1
@rfabbri ฉันไม่ได้แนะนำให้คุณใช้ PETSc สำหรับสิ่งนี้เฉพาะสิ่งที่พวกเขาทำในตัวอย่างนั้นเท่านั้นที่สามารถให้คำแนะนำได้ ฉันได้แก้ไขคำตอบเพื่อให้ชัดเจนยิ่งขึ้น
Daniel Shapero

4

ความคิดอื่นอาจใช้วิธีกำเนิด (โปรแกรมเขียนโปรแกรม) ผู้เขียนโปรแกรม (เมตาดาต้า) ที่แยกลำดับของคำสั่ง C / C ++ เพื่อทำการ unpivoted ** LU บนระบบ 10x10 .. โดยทั่วไปจะใช้รังวน k / i / j และทำให้แบนเป็น O (1,000) หรือมากกว่านั้น เลขคณิตแบบสเกลาร์ จากนั้นให้ฟีดโปรแกรมที่สร้างขึ้นมาเพื่อเพิ่มประสิทธิภาพคอมไพเลอร์ สิ่งที่ฉันคิดว่าน่าสนใจอยู่ที่นี่คือการลบลูปจะเปิดเผยการพึ่งพาข้อมูลและนิพจน์ย่อยซ้ำซ้อนและให้โอกาสสูงสุดแก่คอมไพเลอร์ในการเรียงลำดับคำสั่งใหม่เพื่อให้แมปกับฮาร์ดแวร์จริง (เช่นจำนวนหน่วยปฏิบัติการอันตราย / แผงลอย) บน).

หากคุณรู้ว่าเมทริกซ์ทั้งหมด (หรือแม้แต่เพียงไม่กี่ตัวเท่านั้น) คุณสามารถปรับปรุงทรูพุตได้โดยการเรียก SIMD intrinsics / function (SSE / AVX) แทนรหัสสเกลาร์ ที่นี่คุณจะใช้ประโยชน์จากความขนานที่น่าอับอายในอินสแตนซ์แทนที่จะไล่จับคู่ขนานใด ๆ ภายในอินสแตนซ์เดียว ตัวอย่างเช่นคุณสามารถดำเนินการคู่ที่มีความแม่นยำสูงถึงสองเท่าของ LU ในเวลาเดียวกันโดยใช้ AVX256 ภายในโดยบรรจุเมทริกซ์ 4 ตัว "ข้าม" ลงทะเบียนและดำเนินการแบบเดียวกัน ** กับพวกเขาทั้งหมด

** ดังนั้นการมุ่งเน้นไปที่ LU ที่ไม่ได้ลงทะเบียน หมุนเหวี่ยงริบวิธีนี้ในสองวิธี ครั้งแรกมันแนะนำสาขาเนื่องจากการเลือกเดือยหมายถึงการพึ่งพาข้อมูลของคุณไม่เป็นที่รู้จักอย่างสมบูรณ์ ประการที่สองก็หมายความว่า "ช่อง" SIMD ที่แตกต่างกันจะต้องทำสิ่งที่แตกต่างกันเนื่องจากอินสแตนซ์ A อาจหมุนต่างจากอินสแตนซ์ B ดังนั้นถ้าคุณทำสิ่งนี้ฉันจะแนะนำแบบคงที่ ของแต่ละคอลัมน์เป็นแนวทแยง)


เนื่องจากเมทริกซ์มีขนาดเล็กบางทีการหมุนอาจทำได้ถ้าปรับขนาดไว้ล่วงหน้า ไม่แม้แต่จะหมุนเมทริกซ์ล่วงหน้า สิ่งที่เราต้องการคือรายการนั้นมีขนาดภายใน 2-3 คำสั่งเท่านั้น
rfabbri

2

คำถามของคุณนำไปสู่ข้อพิจารณาที่แตกต่างกันสองข้อ

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

ประการที่สองคุณต้องใช้อัลกอริทึมอย่างมีประสิทธิภาพ ในการทำเช่นนั้นคุณจำเป็นต้องรู้ถึงคอขวดของอัลกอริทึมของคุณ การใช้งานของคุณถูกผูกไว้ด้วยความเร็วของการถ่ายโอนหน่วยความจำหรือความเร็วในการคำนวณ ตั้งแต่คุณพิจารณาเท่านั้น10×10เมทริกซ์ของคุณควรพอดีกับแคชของ CPU อย่างสมบูรณ์ ดังนั้นคุณควรใช้ประโยชน์จากหน่วย SIMD (SSE, AVX, ฯลฯ ) และแกนประมวลผลของคุณเพื่อทำการคำนวณเป็นจำนวนมากต่อรอบที่เป็นไปได้

โดยรวมแล้วคำตอบสำหรับคำถามของคุณนั้นขึ้นอยู่กับฮาร์ดแวร์และเมทริกซ์ที่คุณพิจารณา อาจไม่มีคำตอบที่ชัดเจนและคุณต้องลองบางสิ่งเพื่อค้นหาวิธีการที่เหมาะสมที่สุด


จนถึงตอนนี้ Eigen ได้ปรับใช้อย่างหนักแล้วใช้ SEE, AVX และอื่น ๆ และฉันลองใช้วิธีการวนซ้ำในการทดสอบเบื้องต้นและพวกเขาก็ไม่ได้ช่วยอะไร ฉันลอง Intel MKL แต่ไม่ดีไปกว่า Eigen ด้วยการปรับตั้งค่าสถานะ GCC ฉันกำลังพยายามทำสิ่งที่ดีกว่าและง่ายกว่า Eigen และทำการทดสอบอย่างละเอียดมากขึ้นด้วยวิธีการวนซ้ำ
rfabbri

1

ฉันจะลองทำสิ่งที่ตรงกันข้ามกับ Blockwise

https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion

Eigen ใช้ชุดคำสั่งที่ปรับให้เหมาะสมเพื่อคำนวณอินเวอร์สของเมทริกซ์ 4x4 ซึ่งน่าจะดีที่สุดที่คุณจะได้รับ ลองใช้มันให้มากที่สุด

http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html

ด้านซ้ายบน: 8x8 ด้านบนขวา: 8x2 ด้านล่างซ้าย: 2x8 ด้านล่างขวา: 2x2 ย้อนกลับ 8x8 โดยใช้รหัสผกผัน 4x4 ที่ปรับให้เหมาะสม ส่วนที่เหลือเป็นผลิตภัณฑ์เมทริกซ์

แก้ไข: การใช้บล็อค 6x6, 6x4, 4x6 และ 4x4 แสดงให้เห็นว่าเร็วกว่าที่ฉันได้อธิบายไว้ข้างต้นเล็กน้อย

using namespace Eigen;

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> blockwise_inversion(const Matrix<Scalar, tl_size, tl_size>& A, const Matrix<Scalar, tl_size, br_size>& B, const Matrix<Scalar, br_size, tl_size>& C, const Matrix<Scalar, br_size, br_size>& D)
{
    Matrix<Scalar, tl_size + br_size, tl_size + br_size> result;

    Matrix<Scalar, tl_size, tl_size> A_inv = A.inverse().eval();
    Matrix<Scalar, br_size, br_size> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<tl_size, tl_size>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<tl_size, br_size>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<br_size, tl_size>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<br_size, br_size>() = DCAB_inv;

    return result;
}

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> my_inverse(const Matrix<Scalar, tl_size + br_size, tl_size + br_size>& mat)
{
    const Matrix<Scalar, tl_size, tl_size>& A = mat.topLeftCorner<tl_size, tl_size>();
    const Matrix<Scalar, tl_size, br_size>& B = mat.topRightCorner<tl_size, br_size>();
    const Matrix<Scalar, br_size, tl_size>& C = mat.bottomLeftCorner<br_size, tl_size>();
    const Matrix<Scalar, br_size, br_size>& D = mat.bottomRightCorner<br_size, br_size>();

    return blockwise_inversion<Scalar,tl_size,br_size>(A, B, C, D);
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_8_2(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 8, 8>& A = input.topLeftCorner<8, 8>();
    const Matrix<Scalar, 8, 2>& B = input.topRightCorner<8, 2>();
    const Matrix<Scalar, 2, 8>& C = input.bottomLeftCorner<2, 8>();
    const Matrix<Scalar, 2, 2>& D = input.bottomRightCorner<2, 2>();

    Matrix<Scalar, 8, 8> A_inv = my_inverse<Scalar, 4, 4>(A);
    Matrix<Scalar, 2, 2> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<8, 8>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<8, 2>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<2, 8>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<2, 2>() = DCAB_inv;

    return result;
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_6_4(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 6, 6>& A = input.topLeftCorner<6, 6>();
    const Matrix<Scalar, 6, 4>& B = input.topRightCorner<6, 4>();
    const Matrix<Scalar, 4, 6>& C = input.bottomLeftCorner<4, 6>();
    const Matrix<Scalar, 4, 4>& D = input.bottomRightCorner<4, 4>();

    Matrix<Scalar, 6, 6> A_inv = my_inverse<Scalar, 4, 2>(A);
    Matrix<Scalar, 4, 4> DCAB_inv = (D - C * A_inv * B).inverse().eval();

    result.topLeftCorner<6, 6>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<6, 4>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<4, 6>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<4, 4>() = DCAB_inv;

    return result;
}

นี่คือผลลัพธ์ของการรัน bench bench หนึ่งครั้งโดยใช้Eigen::Matrix<double,10,10>::Random()เมทริกซ์และEigen::Matrix<double,10,1>::Random()เวกเตอร์หนึ่งล้านตัว ในการทดสอบทั้งหมดของฉันการผกผันของฉันจะเร็วกว่าเสมอ รูทีนการแก้ของฉันเกี่ยวข้องกับการคำนวณอินเวอร์สแล้วคูณมันด้วยเวกเตอร์ บางครั้งมันเร็วกว่า Eigen บางครั้งมันก็ไม่ได้ วิธีการทำเครื่องหมายผู้พิพากษาของฉันอาจมีข้อบกพร่อง (ไม่ได้ปิดการใช้งานเทอร์โบบูสต์ ฯลฯ ) นอกจากนี้ฟังก์ชั่นแบบสุ่มของ Eigen อาจไม่แสดงข้อมูลจริง

  • Eigen pivot ผกผันบางส่วน: 3036 มิลลิวินาที
  • ค่าผกผันของฉันกับบล็อกส่วนบน 8x8: 1638 มิลลิวินาที
  • ค่าผกผันของฉันกับบล็อกส่วนบน 6x6: 1234 มิลลิวินาที
  • Eigen เดือยบางส่วนแก้ปัญหา: 1791 มิลลิวินาที
  • การแก้ปัญหาด้วยบล็อกส่วนบนของฉัน 8x8: 1739 มิลลิวินาที
  • การแก้ปัญหาของฉันด้วยบล็อกส่วนบน 6x6: 1286 มิลลิวินาที

ฉันสนใจมากที่จะดูว่าใครสามารถเพิ่มประสิทธิภาพต่อไปนี้ได้เพราะฉันมีแอพพลิเคชั่นที่ จำกัด ซึ่งกลับค่าเมทริกซ์ 10x10 gazillion (และใช่ฉันต้องการค่าสัมประสิทธิ์ของอินเวอร์สทีละตัวดังนั้นการแก้ไขระบบเชิงเส้น .

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.