從CPU和TPU的不同語言抽象看抽象原則
最近和人討論CPU和TPU的語言抽象,把一些總結整理在這裡。
CPU的語言是個時間模型,我隨便拷貝一段Linux的程式碼來作為例子:
static __always_inline long __get_user_pages_locked(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, unsigned long nr_pages, struct page **pages, struct vm_area_struct **vmas, int *locked, unsigned int flags) { long ret, pages_done; bool lock_dropped; if (locked) { /* if VM_FAULT_RETRY can be returned, vmas become invalid */ BUG_ON(vmas); /* check caller initialized locked */ BUG_ON(*locked != 1); } if (pages) flags |= FOLL_GET; pages_done = 0; lock_dropped = false; for (;;) { ret = __get_user_pages(tsk, mm, start, nr_pages, flags, pages, vmas, locked); if (!locked) /* VM_FAULT_RETRY couldn't trigger, bypass */ return ret; /* VM_FAULT_RETRY cannot return errors */ if (!*locked) { BUG_ON(ret < 0); BUG_ON(ret >= nr_pages); } if (!pages) /* If it's a prefault don't insist harder */ return ret; if (ret > 0) { nr_pages -= ret; pages_done += ret; if (!nr_pages) break; } if (*locked) { /* * VM_FAULT_RETRY didn't trigger or it was a * FOLL_NOWAIT. */ if (!pages_done) pages_done = ret; break; } /* VM_FAULT_RETRY triggered, so seek to the faulting offset */ pages += ret; start += ret << PAGE_SHIFT; /* * Repeat on the address that fired VM_FAULT_RETRY * without FAULT_FLAG_ALLOW_RETRY but with * FAULT_FLAG_TRIED. */ *locked = 1; lock_dropped = true; down_read(&mm->mmap_sem); ret = __get_user_pages(tsk, mm, start, 1, flags | FOLL_TRIED, pages, NULL, NULL); if (ret != 1) { BUG_ON(ret > 1); if (!pages_done) pages_done = ret; break; } nr_pages--; pages_done++; if (!nr_pages) break; pages++; start += PAGE_SIZE; } if (lock_dropped && *locked) { /* * We must let the caller know we temporarily dropped the lock * and so the critical section protected by it was lost. */ up_read(&mm->mmap_sem); *locked = 0; } return pages_done; }
可以看到,這種“CPU程式碼”,語句上的相關性是極強的:我取一個頁,如果取不到,就告訴使用者失敗,取到了,對這個頁裡面的幾個域進行賦值,如果是情況A,給這個值。如果是情況B,給那個值。所以,“CPU程式碼”,你給我10個乘法器,這東西是沒有什麼意義的,反正我大部分時候都是在做判斷,前一個判斷沒有做完前,反正我也不能做下一個判斷。而且這恰恰是人類理性思考的特徵,我能給你的明確控制就是這樣的,這個東西改變不了。人的理智永遠不能思考“下意識”:看見紅的東西,感受到了溫度,想起自己正在鍋爐房裡面,手想都不想就可以縮回來,這個用人腦是做不到的,下意識怎麼做到的,人腦是想不清楚的。
所以,編譯器對CPU程式碼的排程。主要是排程暫存器:我有32個暫存器,你要做一組連續的控制,我也就能保證你少數幾條不相關的指令,不會因為有限的暫存器而產生互相依賴。我只能保證我把你記憶體中的資料(因為要在CPU中執行)儘量地分開給你的暫存器,保證沒有依賴的兩個指令,可以被獨立的執行部件來執行。至於這些部件的利用效率,其實我是不怎麼在乎的,因為這沒啥意義。
限制CPU執行並行度提升的不是CPU的結構,而是人腦。我給你的就是連續的,有依賴的執行序列,你不能怎麼樣。C語言也是基於這個邏輯來設計的,它左右了你的並行度提升。它主要提升並行度的手段,更多是執行緒,執行緒本質也是線性依賴的,只是可以支援很多的線性依賴,把資料完全獨立掉,從而有多個並行獨立實體。這仍是線性思維為中心的。
TPU則不同,比如它裡面有100個卷積計算器,它的整個目的就是認為你會有100個同步並行的卷積運算可以同時進行。那麼在語義上,你就得能給出這100個卷積,編譯器才有可能排程TPU裡面的快取或者暫存器,實現對所有執行部件的排程。
所以,TPU的排程,無論你怎麼設計,離不開對執行體和緩衝區的排程。你不能離開這一個特點來給TPU提供描述。但對TPU來說,執行體的數量和緩衝區的大小,必然是會升級的。把這一層資訊給開發者,就相當於告訴別人不要升級了。就算你要你說你可以分兩層,先給一個C語言層,然後再給一個高層語言排程為C語言,這也沒有意義,因為C語言那一層沒有人會寫程式,真要寫的要不是彙編,要不是高層語言。一個可以被生成的語言,對開發者是沒有意義的。所以,做一個類似CPU的表述層,這件事本身對TPU來說沒有意義。
然後我們考慮第二個問題,我們是通過提供一組向量計算的方式讓編譯器進行排程呢?還是提供一組執行緒來給TPU OS(或者叫Runtime也行)進行排程呢?
如前所述,執行緒的本質仍是線性思維,一旦你靠執行緒來給TPU做排程,就意味著使用者需要認知你的緩衝區的使用了。這違背了我們前面說的,緩衝區的大小,跨代肯定是不同的。
你看,我們簡單這樣推演一下,你就會發現,其實從我們確定了TPU要解決的問題的時候,無論你用什麼技巧,你的選擇就只剩下一個:讓使用者把整片的,需要進行向量計算的要求,整體提供給你的編譯器,讓它重排到你的TPU上。這個過程當然要對大量的使用者例項進行分析,基於這個來確定語法乃至決定TPU的硬體配置。但無論如何,其他路是一定不通的,根本沒有必要走。
抽象,通常就是這麼個東西,所有東西都是可變的,我們要抓住主要矛盾和矛盾的主要方面,所以抽象的中心是需求,而不是現在的硬體做成什麼樣。
版本控制:
V1:完成了初稿,把骨幹架起來了,其他細節待補。