छोटे वर्ग मैट्रिसेस (10x10) के लिए सबसे तेज़ लीनियर सिस्टम हल


9

मुझे छोटे मेट्रिसेस (10x10) के लिए लीनियर सिस्टम सॉल्यूशन से नर्क को अनुकूलित करने में बहुत दिलचस्पी है, जिसे कभी-कभी छोटे मैट्रिस भी कहा जाता है । क्या इसके लिए कोई तैयार समाधान है? मैट्रिक्स को नॉनसिंगुलर माना जा सकता है।

इस सोलवर को इंटेल सीपीयू पर माइक्रोसेकंड में 1 000 000 से अधिक बार निष्पादित किया जाना है। मैं कंप्यूटर गेम में उपयोग किए जाने वाले अनुकूलन के स्तर पर बात कर रहा हूं। कोई फर्क नहीं पड़ता कि मैं इसे असेंबली और आर्किटेक्चर-विशिष्ट में कोड करता हूं, या सटीक या विश्वसनीयता ट्रेडऑफ़्स का अध्ययन करता हूं और फ्लोटिंग पॉइंट हैक का उपयोग करता हूं (मैं -फ़ास्ट-गणित संकलन ध्वज का उपयोग करता हूं, कोई समस्या नहीं)। समाधान भी लगभग 20% समय के लिए विफल हो सकता है!

Eigen का आंशिकपीवू मेरे वर्तमान बेंचमार्क में सबसे तेज़ है, -O3 और एक अच्छे कंपाइलर के साथ अनुकूलित होने पर LAPACK से बेहतर प्रदर्शन करता है। लेकिन अब मैं एक कस्टम रैखिक सॉल्वर को सौंपने के बिंदु पर हूं। किसी भी सलाह की काफी सराहना की जाएगी। मैं अपने समाधान को खुला स्रोत बनाऊंगा और प्रकाशनों आदि में महत्वपूर्ण जानकारियाँ दूंगा।

संबंधित: ब्लॉक विकर्ण मैट्रिक्स के साथ रैखिक प्रणाली को हल करने की गति लाखों मैट्रिसेस को पलटने का सबसे तेज़ तरीका क्या है? https://stackoverflow.com/q/50909385/1489510


7
यह एक खिंचाव लक्ष्य की तरह दिखता है। मान लेते हैं कि हम 4 एकल-सटीक TFLOPs के सैद्धांतिक शिखर थ्रूपुट के साथ सबसे तेज़ स्काईलेक-एक्स ज़ीओन प्लेटिनम 8180 का उपयोग करते हैं, और यह कि 10x10 सिस्टम के लिए लगभग 700 (लगभग 2n ** 3/3) फ्लोटिंग-पॉइंट ऑपरेशन को हल करने की आवश्यकता होती है। तब 1M बैच का सिस्टम सैद्धांतिक रूप से 175 माइक्रोसेकंड में हल किया जा सकता था। यह एक से अधिक गति से प्रकाश संख्या नहीं है। क्या आप साझा कर सकते हैं कि आप वर्तमान में अपने सबसे तेज़ मौजूदा कोड के साथ क्या प्रदर्शन प्राप्त कर रहे हैं? BTW, डेटा एकल परिशुद्धता या दोहरी परिशुद्धता है?
njuffa

@njuffa हां मैंने 1ms के करीब पहुंचने का लक्ष्य रखा लेकिन माइक्रो एक और कहानी है। सूक्ष्म के लिए मैंने समान मैट्रिक्स का पता लगाकर बैच में वृद्धिशील व्युत्क्रम संरचना का शोषण करने पर विचार किया, जो अक्सर होता है। प्रोसेसर के आधार पर 10-500ms की रेंज में परफ़ेक्ट रूप से कर्मी होता है। परिशुद्धता डबल या यहां तक ​​कि जटिल डबल है। एकल परिशुद्धता धीमी है।
rfabbri

@njuffa मैं गति के लिए परिशुद्धता को कम या बढ़ा सकता हूं
rfabbri

2
ऐसा लगता है कि सटीकता / सटीकता आपकी प्राथमिकता नहीं है। आपके लक्ष्य के लिए, मूल्यांकन के अपेक्षाकृत कम संख्या में छंटनी की गई पुनरावृत्ति विधि उपयोगी हो सकती है? खासकर यदि आपके पास एक उचित प्रारंभिक अनुमान है।
स्पेंसर ब्रायंजेलसन

1
क्या आप धुरी हैं? क्या आप गाऊसी उन्मूलन के बजाय एक क्यू कारककरण कर सकते हैं। क्या आप अपने सिस्टम को इंटरलेक्ट करते हैं ताकि आप एक बार में सिमड के निर्देशों का उपयोग कर सकें और कई सिस्टम कर सकें? क्या आप बिना लूप और अप्रत्यक्ष पते के साथ सीधी-रेखा वाले प्रोग्राम लिखते हैं? आप क्या सटीकता चाहते हैं और मैं कैसे वातानुकूलित हूँ आपके सिस्टम? क्या उनके पास कोई संरचना है जिसका शोषण किया जा सकता है।
कार्ल क्रिश्चियन

जवाबों:


7

Eigen मैट्रिक्स प्रकार का उपयोग करना जहां संकलित समय में पंक्तियों और स्तंभों की संख्या टाइप की जाती है , आपको LAPACK पर बढ़त मिलती है, जहां मैट्रिक्स का आकार केवल रनटाइम पर जाना जाता है। यह अतिरिक्त जानकारी संकलक को पूर्ण या आंशिक लूप को अनियंत्रित करने की अनुमति देती है, जिससे शाखा के बहुत सारे निर्देश समाप्त हो जाते हैं। यदि आप अपनी खुद की गुठली लिखने के बजाय एक मौजूदा पुस्तकालय का उपयोग कर रहे हैं, तो एक डेटा प्रकार जहां मैट्रिक्स आकार को C ++ टेम्पलेट पैरामीटर के रूप में शामिल किया जा सकता है, वह संभवतः आवश्यक होगा। केवल एक अन्य पुस्तकालय जिसके बारे में मुझे पता है कि यह धमाका है , इसलिए यह Eigen के खिलाफ बेंचमार्किंग के लायक हो सकता है।

यदि आप अपने स्वयं के कार्यान्वयन को रोल करने का निर्णय लेते हैं, तो आप पा सकते हैं कि पेट्सक अपने ब्लॉक सीएसआर प्रारूप के लिए एक उपयोगी उदाहरण क्या है, हालांकि पेट्सक खुद संभवतः आपके लिए सही उपकरण नहीं होगा जो आपके पास है। लूप लिखने के बजाय, वे छोटे मैट्रिक्स-वेक्टर गुणकों के लिए हर एक ऑपरेशन को स्पष्ट रूप से लिखते हैं ( इस फाइल को अपने रिपॉजिटरी में देखें)। यह गारंटी देता है कि कोई शाखा निर्देश नहीं हैं जैसे आप एक लूप के साथ प्राप्त कर सकते हैं। AVX निर्देशों के साथ कोड के संस्करण वास्तव में वेक्टर एक्सटेंशन का उपयोग करने का एक अच्छा उदाहरण है। उदाहरण के लिए, यह फ़ंक्शन उपयोग करता है__m256dडेटा प्रकार एक साथ एक ही समय में चार डबल्स पर काम करते हैं। आप वेक्टर एक्सटेंशन का उपयोग करके सभी कार्यों को स्पष्ट रूप से लिखकर एक सराहनीय प्रदर्शन को बढ़ावा दे सकते हैं, केवल मैट्रिक्स-वेक्टर बहु ​​के बजाय एलयू फैक्टराइजेशन के लिए। वास्तव में सी कोड को हाथ से लिखने के बजाय, आप इसे बनाने के लिए स्क्रिप्ट का उपयोग करना बेहतर समझेंगे। यह देखने के लिए भी मजेदार हो सकता है कि जब आप निर्देश पाइपलाइनिंग का बेहतर लाभ उठाने के लिए कुछ ऑपरेशंस को पुनः व्यवस्थित करते हैं तो एक सराहनीय प्रदर्शन अंतर होता है या नहीं।

आप उपकरण STOKE से कुछ माइलेज भी प्राप्त कर सकते हैं , जो कि तेजी से संस्करण खोजने के लिए संभावित प्रोग्राम परिवर्तनों के स्थान को बेतरतीब ढंग से पता लगाएगा


tx। मैं पहले से ही Eigen जैसे मानचित्र <const मैट्रिक्स <जटिल, 10, 10>> AA (ए) का सफलतापूर्वक उपयोग करता हूं। अन्य सामान की जाँच करेगा।
rfabbri

Eigen में AVX भी है और यहां तक ​​कि इसके लिए एक complex.h हैडर भी है। इसके लिए PETSc क्यों? इस मामले में Eigen से मुकाबला करना कठिन है। मैंने अपनी समस्या के लिए Eigen को और भी अधिक विशेष कर दिया और एक अनुमानित धुरी रणनीति के साथ कि एक कॉलम पर अधिकतम लेने के बजाय, एक धुरी को तुरंत स्वैप करता है जब वह एक और पाता है जो परिमाण के 3 आदेशों को बड़ा करता है।
rfabbri

1
@rfabbri मैं यह सुझाव नहीं दे रहा था कि आप इसके लिए PETSc का उपयोग करें, केवल उस विशेष उदाहरण में वे जो करते हैं वह शिक्षाप्रद हो सकता है। मैंने उत्तर को स्पष्ट करने के लिए संपादित किया है।
डैनियल शेपरो

4

एक और विचार एक जनरेटिव एप्रोच (एक प्रोग्राम लिखने वाला प्रोग्राम) का उपयोग किया जा सकता है। लेखक (मेटा) प्रोग्राम जो सी / सी ++ निर्देशों का क्रम समाप्त करता है, एक 10x10 सिस्टम पर ** ** एलयू करने के लिए निर्देश देता है .. मूल रूप से के / आई / जे लूप घोंसला ले रहा है और इसे ओ (1000) या लाइनों में समतल कर रहा है। स्केलर अंकगणित। फिर उस प्रोग्राम को जो भी कंपाइल करने वाला हो, उसे फीड कर दें। मुझे लगता है कि यहां दिलचस्प है, लूप्स को हटा रहा है, हर डेटा निर्भरता और अनावश्यक उप-एक्सपोज़र को उजागर करता है, और कंपाइलर को निर्देशों को पुन: व्यवस्थित करने के लिए अधिकतम अवसर देता है ताकि वे वास्तविक हार्डवेयर (जैसे निष्पादन इकाइयों, खतरों या स्टालों की संख्या) को अच्छी तरह से मैप करें, ताकि पर)।

यदि आप सभी मैट्रिसेस (या यहां तक ​​कि उनमें से कुछ) को जानते हैं, तो आप स्केलर कोड के बजाय SIMD इंट्रिंसिक्स / फ़ंक्शन (SSE / AVX) को कॉल करके थ्रूपुट में सुधार कर सकते हैं। यहाँ आप एक उदाहरण के भीतर किसी भी समानता का पीछा करने के बजाय, उदाहरणों में शर्मनाक समानता का शोषण कर रहे हैं। उदाहरण के लिए, आप AVX256 आंतरिक का उपयोग करके 4 डबल परिशुद्धता LU का एक साथ प्रदर्शन कर सकते हैं, 4 मैट्रिसेस को "रजिस्टर" भर में पैक करके और उन सभी पर ** एक ही ऑपरेशन ** करके।

** इसलिए अप्रकाशित एलयू पर ध्यान केंद्रित। पिवट करना इस दृष्टिकोण को दो तरह से खराब करता है। सबसे पहले, यह धुरी चयन के कारण शाखाओं का परिचय देता है, जिसका अर्थ है कि आपके डेटा निर्भरताएं पूरी तरह से ज्ञात नहीं हैं। दूसरा, इसका मतलब है कि अलग-अलग SIMD "स्लॉट्स" को अलग-अलग चीजें करनी होंगी, क्योंकि उदाहरण A, उदाहरण B से भिन्न रूप से धुरी कर सकता है। इसलिए यदि आप इनमें से किसी का भी पीछा करते हैं, तो मैं सुझाव देता हूं कि आप गणना से पहले अपने मैट्रिसेस को पिवट करें (सबसे बड़ी प्रविष्टि) प्रत्येक स्तंभ के विकर्ण के लिए)।


चूंकि मैट्रिसेस इतने छोटे होते हैं, शायद अगर वे पूर्व-स्केल किए गए हैं तो धुरी को दूर किया जा सकता है। मेट्रिक्स को प्री-पिवेटिंग भी नहीं। हम सभी की जरूरत है कि प्रविष्टियां एक-दूसरे के परिमाण के 2-3 आदेशों के भीतर हों।
rfabbri

2

आपका प्रश्न दो भिन्न विचारों की ओर ले जाता है।

सबसे पहले, आपको सही एल्गोरिथ्म चुनने की आवश्यकता है। इसलिए, इस सवाल पर कि क्या मेट्रिक्स की कोई संरचना है, इस पर विचार किया जाना चाहिए। उदाहरण के लिए, जब मैट्रिसेस सममित होते हैं, एक चोल्स्की अपघटन एलयू की तुलना में अधिक कुशल होता है। जब आपको केवल सीमित मात्रा में सटीकता की आवश्यकता होती है तो एक पुनरावृत्ति विधि तेज हो सकती है।

दूसरा, आपको एल्गोरिथ्म को कुशलता से लागू करने की आवश्यकता है। ऐसा करने के लिए, आपको अपने एल्गोरिथ्म की अड़चन को जानना होगा। क्या आपका कार्यान्वयन मेमोरी ट्रांसफर की गति से या गणना की गति से बाध्य है। चूंकि आप केवल विचार करते हैं10×10मैट्रिसेस, आपका मैट्रिक्स पूरी तरह से सीपीयू कैश में फिट होना चाहिए। इस प्रकार, आपको अपने प्रोसेसर के सिमड इकाइयों (एसएसई, एवीएक्स, आदि) और कोर का उपयोग करना चाहिए, जितना संभव हो प्रति चक्र प्रति गणना करने के लिए।

कुल मिलाकर, आपके प्रश्न का उत्तर उस हार्डवेयर और मैट्रेस पर निर्भर करता है, जिस पर आप विचार करते हैं। शायद कोई निश्चित जवाब नहीं है और आपको एक इष्टतम तरीका खोजने के लिए कुछ चीजों को आज़माना होगा।


अब तक ईजन पहले से ही बहुत अधिक अनुकूलन करता है, एसईई, एवीएक्स आदि का उपयोग करता है और मैंने प्रारंभिक परीक्षा में पुनरावृत्त तरीकों की कोशिश की और उन्होंने मदद नहीं की। मैं इंटेल MKL की कोशिश की, लेकिन अनुकूलित GCC झंडे के साथ Eigen से बेहतर नहीं है। मैं वर्तमान में Eigen की तुलना में कुछ बेहतर और सरल बनाने और पुनरावृत्तियों के साथ अधिक विस्तृत परीक्षण करने की कोशिश कर रहा हूं।
rfabbri

1

मैं ब्लॉकव्यू उलटा करने की कोशिश करूंगा।

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। अनुकूलित 4x4 उलटा कोड का उपयोग करके 8x8 को पलटें। बाकी मैट्रिक्स उत्पाद है।

संपादित करें: 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;
}

यहां दस लाख Eigen::Matrix<double,10,10>::Random()मैट्रिस और Eigen::Matrix<double,10,1>::Random()वैक्टर का उपयोग करके एक बेंच मार्क रन के परिणाम दिए गए हैं । मेरे सभी परीक्षणों में, मेरा व्युत्क्रम हमेशा तेज होता है। मेरी सुलझी हुई दिनचर्या में व्युत्क्रम की गणना करना और फिर इसे एक वेक्टर द्वारा गुणा करना शामिल है। कभी इजन से तेज तो कभी इसका। मेरी बेंच मार्किंग विधि त्रुटिपूर्ण हो सकती है (टर्बो बूस्ट को अक्षम न करें, आदि)। इसके अलावा, Eigen के यादृच्छिक कार्य वास्तविक डेटा के प्रतिनिधि नहीं हो सकते हैं।

  • Eigen आंशिक धुरी व्युत्क्रम: 3036 मिलीसेकंड
  • 8x8 ऊपरी ब्लॉक के साथ मेरा उलटा: 1638 मिलीसेकंड
  • 6x6 ऊपरी ब्लॉक के साथ मेरा व्युत्क्रम: 1234 मिली सेकेंड
  • Eigen आंशिक धुरी हल: 1791 मिलीसेकंड
  • 8x8 ऊपरी ब्लॉक के साथ मेरा हल: 1739 मिलीसेकंड
  • 6x6 ऊपरी ब्लॉक के साथ मेरा समाधान: 1286 मिलीसेकंड

मुझे यह देखने के लिए बहुत दिलचस्पी है कि क्या कोई इसे आगे भी अनुकूलित कर सकता है, क्योंकि मेरे पास एक परिमित तत्व अनुप्रयोग है जो एक gazillion 10x10 matrices (और हाँ, मुझे व्युत्क्रम के व्यक्तिगत गुणांक की आवश्यकता है, इसलिए सीधे एक रैखिक प्रणाली को हल करना एक विकल्प नहीं है) ।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.