Hastlayer ile FPGA Programlama
Günümüzde artık her elektronik cihazda işlemci(CPU) ile karşılaşmanız mümkün. Cep telefonu, akıllı saat, buzdolabı, hatta asansörlerde dahi CPU var. Sabit donanım tasarımı ve veri yolu (32 bit, 64 bit) sayesinde uygun şekilde yazılan bir kod bu cihazlarda kolaylıkla çalışabiliyor. Diğer yandan görüntü işleme ve yorumlama gibi hesaplama bazlı işlemlerde CPU’nun oldukça yavaş kaldığını söyleyebiliriz. Peki Tesla gibi sürücüsüz araç üreten firmalar kamera verilerini alıp yoğun hesaplama gerektiren işlemleri CPU’ya mı yaptırıyorlar dersiniz?
FPGA nedir?
FPGA (Field Programmable Gate Array) kavramını “kullanım alanına özel olarak programlanabilir mantıksal kapı dizileri” şeklinde çevirsek yanlış olmaz. FPGA’ler, CPU’ların aksine sabit bir donanım tasarımına sahip değillerdir. Bunun aksine kullanıcı uygulamalarına göre sıfırdan programlanabilirler. Mantıksal hücrelerin gerçekleştireceği fonksiyonlar ve bu foksiyonlar arasındaki bağlantılar kullanıcı tarafından belirlenir. Bu sebeple programcının kod yazarken kullandığı çarp böl gibi aritmetik işlemler FPGA programlanırken sıfırdan kodlanmak zorundadır. Örneğin aşağıdaki VHDL kodunda bir AND kapısının nasıl tasarlandığını görebilirsiniz:
-- (this is a VHDL comment) -- import std_logic from the IEEE library library IEEE; use IEEE.std_logic_1164.all; -- this is the entity entity ANDGATE is port ( I1 : in std_logic; I2 : in std_logic; O : out std_logic); end entity ANDGATE; -- this is the architecture architecture RTL of ANDGATE is begin O <= I1 and I2; end architecture RTL;
Programcı günlük hayatta AND işlemi için üstteki gibi bir kodla uğraşmaz. Zaten masaüstü geliştirme, web geliştirime gibi genel uygulamalalarda böyle bir tanımın yapılmasına da gerek yoktur. Fakat X-Ray cihazı gibi medikal bir ürün tasarlıyorsanız ve donanımdan maksimum verim elde etmek istiyorsanız üstteki kod gibi süreçleri oluşturup paralel olarak kodlayıp yönetmeniz kaçınılmazdır.
Birçoğunuz (ben dahil) FPGA’leri ilk gördüğünüzde Arduino veya Raspberry Pi‘a benzetebilirsiniz. Arduino bir mikrokontrolcü, Raspberry Pi ise bir bilgisayar olduğu için FPGA ile bu benzetmelerin alakası yoktur.
Neden FPGA?
Görüntü işleme gibi bir süreci GPU’ya yaptırmak veya hesaplamalar ile ilgili süreçleri CPU’ya yaptırmak varken neden FPGA kodlayasınız? Aslında bunun oldukça mantıklı sebepleri var. Dilerseniz bir görsel ile aradaki farkı ortaya koyalım:
Üstteki görseli baz alacak olursak CPU, GPU ve FPGA arasındaki farkı 3 kategoride değerlendirmemiz mümkün olacaktır:
- Paralellik: Çok çekirdekli CPU’larda yazılan uygulamanın ilgili süreçlerinin çekirdekler arası dağıtılması ile “paralelimsi” bir uygulama elde edilmiş oluyor. CPU’ların aksine GPU’larda binlerce çekirdek yer aldığı için piksek piksel görüntü işleme veya bitcoin madenciliği gibi hesaplama bazlı işlemlerde gerçek paralelliğe kavuşulmuş oluyor. FPGA’lerde ise durum biraz daha farklı. Çekirdeklerin aksine mantıksal kapılar ile oluşturulan süreçlerin paralelliği kod bazında sağlanmış oluyor. Yani çekirdek değil transistör temelinde paralellik oluşturuluyor. CPU’lardan biraz daha fazla paralellik sunan bu yöntem, GPU’lardan biraz daha düşük olarak paralelliğin uygulanması ile orta yol bulunmuş oluyor.
- Program karmaşıklığı: CPU’lar herhangi bir kodu rahatça uyarlayıp çalıştırabildikleri için uygulama karmaşıklığı istenilen her seviyede olabilir. GPU programlama ile uğraşanların bildiği gibi oluşturulan uygulama için birçok kısıtlamalar vardır ve yapabilecekleriniz sınırlıdır. FPGA’lerde ise bu durum daha orta hallidir.
- Güç kullanımından alınan verim: FPGA’ler için diğer rakiplerine fark atan belki de en önemli kategorinin bu olduğunu söyleyebiliriz. CPU ve GPU’ların birçok farklı yararı olduğu aşikârdır. Güç kullanımından alınan verim, yazılan uygulamanın kendisi ile doğrudan bağlantılıdır. Fakat FPGA’lerde ise donanım tasarımını istenilen amaca göre yapılandırdığınız için diğer her iki işlem biriminin aksine güç verimliliği çok çok daha fazladır. Genellikle CPU ve GPU herhangi bir işlemi yapmak için uygundur, fakat FPGA’de ise sadece tanımladığınız belirli bir görev için uyumluluk vardır. Bu sayede güç kullanımı açısından çok verimli ürünler ortaya çıkarmanız mümkündür. Ayrıca FPGA’ler, USB’den gücünü alan ve birkaç volt seviyesinde oldukça düşük güç tüketen cihazlardır. Bu nedenle bazı FPGA’ler, megahertz’ler seviyesinde düşük çalışma hızına sahiptirler.
FPGA’lerin kadar yararı varken tabi ki büyük bir negatif yönü var. CPU için program geliştirirken bazı temel yazılım geliştirme bilgisine sahip olmak yeterlidir. GPU için de benzer bir durum var. Fakat FPGA’lere geldiğimizde aradaki geliştirme farklılığın oldukça arttığını söyleyebiliriz. Bu farklılığın temelinde, geliştiricinin FPGA için yazılım geliştirme bilgisinin yeterli olmaması, donanım mühendislerinin kabiliyetlerine gereksinim duyması vardır. Evet VHDL kodu da temelinde bir “kod”‘dur. Fakat gerçek şu ki; bu kodu yazarken yazılım geliştiriminde hiç düşünmediğiniz AND kapıları veya OR kapılarının birbirine bağlanması gibi çok daha ilkel seviyedeki işleri düşünmek zorunda olmanızdır. Tam bu noktada Hastlayer gibi yüksek seviyeli araçlar ile aynı CPU için kodlama yapar gibi FPGA kartı kodlayabiliyorsunuz.
Hastlayer Nedir?
Hastlayer, kurucuları Zoltán Lehóczky ve Benedek Farkas olan Lombiq firması tarafından geliştirilen, yazılan uygulama kodunu alıp FPGA mantık kapılarına dönüştüren bir yapıdır. Yazdığınız kodun illa ki bir .NET kodu olması gerekli değildir. Farklı diller henüz Hastlayer içerisinde denenmediği için teorik olarak .NET assembly diline dönüşebilecek her dil (C#, VB, C++, F#, Python, PHP, JavaScript vs.) Hastlayer ile FPGA koduna dönüştürülebilir diyebiliriz.
Peki neden bildiğimiz dilde kod yazıp CPU’da çalıştırmak varken FPGA kodu yazmak için Hastlayer kullanalım? Eğer yapacağınız işlem çok fazla hesaplama bazlı ise, FPGA kullanmanın CPU ve GPU’ya göre birçok performans avantajı vardır. Ayrıca sadece yapılandırdığınız şekilde işlemleri yürüttüklerinden dolayı hem daha az güç tüketirler hem de ısınma problemleri CPU ve GPU’ya göre çok daha azdır. Tek bir bilgisayarda yürütülen algoritma için güç tüketimi önemsizdir. Fakat on binlerce sunucuda çalışacak bilimsel simülasyonları baz alırsak, 10 watt kullanan ile 100 watt kullanan donanımın farkı büyük ölçeklerde önemli ölçüde hissedilecektir. Şimdi isterseniz Hastlayer’ın kullanımına gelelim.
Hastlayer Gereksinimleri
Hastlayer ile FPGA kodlayabilmek için aşağıdaki gereksinimlere ihtiyacınız var:
- Hastlayer SDK ve Xilinx Hardware Framework‘ü indirmeniz gerekiyor.
- .NET’ten donanım koduna çevrim yapacak Hastlayer Servislerine erişiminiz olması gerekiyor. Şu anda değerlendirme erişimi ücretsiz sunuluyor ve [email protected] üzerinden ilgili erişimi sağlayabiliyorsunuz.
- Uyumlu bir FPGA geliştirme kartına ihtiyacınız var. Şu anda sadece Nexys 4 DDR kartı Hastlayer tarafından destekleniyor. Kart piyasadaki diğer FPGA kartlara kıyasla daha düşük bir çalıştırma frekansına sahip olduğu için devasa algoritmalar için elverişli olmadığını belirtmekte yarar var.
Eğer uygun bir FPGA kartınız varsa, az sonra anlatılacak olan örnek uygulamayı Hastlayer servislerine erişiminiz olmadan dahi çalıştırabilirsiniz.
Örnek proje
Aşağıdaki şekilde paralel çalışacak bir algoritmamız olduğunu ele alalım:
using System.Threading.Tasks; using Hast.Transformer.Abstractions.SimpleMemory; namespace Hast.Samples.SampleAssembly { public class ParallelAlgorithm { public const int Run_InputUInt32Index = 0; public const int Run_OutputUInt32Index = 0; public const int MaxDegreeOfParallelism = 280; public virtual void Run(SimpleMemory memory) { var input = memory.ReadUInt32(Run_InputUInt32Index); var tasks = new Task<uint>[MaxDegreeOfParallelism]; for (uint i = 0; i < MaxDegreeOfParallelism; i++) { tasks[i] = Task.Factory.StartNew( indexObject => { var index = (uint)indexObject; uint result = input + index * 2; var even = true; for (int j = 2; j < 9999999; j++) { result = even ? result + index : result - index; even = !even; } return result; }, i); } Task.WhenAll(tasks).Wait(); uint output = 0; for (int i = 0; i < MaxDegreeOfParallelism; i++) { output += tasks[i].Result; } memory.WriteUInt32(Run_OutputUInt32Index, output); } } public static class ParallelAlgorithmExtensions { public static uint Run(this ParallelAlgorithm algorithm, uint input) { var memory = new SimpleMemory(1); memory.WriteUInt32(ParallelAlgorithm.Run_InputUInt32Index, input); algorithm.Run(memory); return memory.ReadUInt32(ParallelAlgorithm.Run_OutputUInt32Index); } } }
Koda baktığımızda oldukça basit bir parallelleştirilmiş kod olduğunu görüyoruz. Sıradan bir C# kodu olmasının yanında Hastlayer’a özel SimpleMemory
nesnesini kullanmakta. FPGA’lerde de bilgisayarlardaki gibi DDR RAM bulunduğu için SimpleMemory nesnesi ile bu RAM’e erişim yapmış oluyoruz. Yani FPGA’e attığımız veriler ve FPGA’den dönen verilere SimpleMemory nesnesi aracılığıyla ulaşabiliyoruz. Kodda da SimpleMemory nesnesinden integer bir değer okuyup input
değişkenine atıyoruz. Geriye kalan kısımları kabaca açıklayacak olursak algoritma, 280 adet Thread oluşturup paralel çalıştıracak şekilde 10 milyon defa çeşitli toplama ve çıkarma işlemleri yapıyor ve Thread çıktılarını topluyor diyebiliriz. Toplam sonucunu SimpleMemory’e tekrar geri yazan kodun 280 adet Thread oluşturması, aslında hepsinin aynı anda çalıştığını göstermeyebilir. Zira bilgisayarınızda CPU bulunduğu için, toplam 4 çekirdek olup, mantıksal çekirdek sayısı 8 olabilir. O halde 8 Thread’i aynı anda çalıştırabilecektir. Oysa ki FPGA’de 280 tane birbirinden ayrı çekirdek birimi oluşturup Thread’leri bu çekirdekler içerisinde çalıştırabiliriz.
Şimdi bu derlemeyi FPGA kartında çalışacak hale getirmek için ilgili konsol uygulamasını oluşturalım:
using System; using System.Threading.Tasks; using Hast.Layer; using Hast.Samples.SampleAssembly; using Hast.Transformer.Abstractions.Configuration; using Hast.Transformer.Vhdl.Abstractions.Configuration; namespace Hast.Samples.Demo { class Program { static void Main(string[] args) { Task.Run(async () => { using (var hastlayer = await Hastlayer.Create()) { #region Configuration var configuration = new HardwareGenerationConfiguration("Nexys4 DDR"); configuration.AddHardwareEntryPointType<ParallelAlgorithm>(); configuration.TransformerConfiguration().AddMemberInvocationInstanceCountConfiguration( new MemberInvocationInstanceCountConfigurationForMethod<ParallelAlgorithm>(p => p.Run(null), 0) { MaxDegreeOfParallelism = ParallelAlgorithm.MaxDegreeOfParallelism }); configuration.VhdlTransformerConfiguration().VhdlGenerationMode = VhdlGenerationMode.Debug; hastlayer.ExecutedOnHardware += (sender, e) => { Console.WriteLine( "Executing " + e.MemberFullName + " on hardware took " + e.HardwareExecutionInformation.HardwareExecutionTimeMilliseconds + "ms (net) " + e.HardwareExecutionInformation.FullExecutionTimeMilliseconds + " milliseconds (all together)"); }; #endregion #region HardwareGeneration Console.WriteLine("Hardware generation starts."); var hardwareRepresentation = await hastlayer.GenerateHardware( new[] { typeof(ParallelAlgorithm).Assembly }, configuration); await hardwareRepresentation.HardwareDescription.WriteSource("Hast_IP.vhd"); #endregion #region Execution Console.WriteLine("Starting hardware execution."); var parallelAlgorithm = await hastlayer.GenerateProxy(hardwareRepresentation, new ParallelAlgorithm()); var output1 = parallelAlgorithm.Run(234234); var output2 = parallelAlgorithm.Run(123); var output3 = parallelAlgorithm.Run(9999); var sw = System.Diagnostics.Stopwatch.StartNew(); var cpuOutput = new ParallelAlgorithm().Run(234234); sw.Stop(); Console.WriteLine("On CPU it took " + sw.ElapsedMilliseconds + "ms."); #endregion } }).Wait(); Console.ReadKey(); } } }
Gördüğünüz gibi çalışacak kodun bir Task içerisinde sarılması gerekiyor. Devamında Hastlayer
instance’ı oluşturup FPGA kartını belirterek, birtakım yapılandırma ayarlarını giriyoruz. Algoritma olarak da daha önce oluşturduğumuz ParallelAlgorithm
sınıfını atıyoruz ve paralellik seviyesi olarak, daha önce 280’i atadığımız MaxDegreeOfParallelism
‘i fonksiyona veriyoruz. Ve kod bu şekilde devam ediyor.
Uygulamadaki en önemli nokta ise typeof(ParallelAlgorithm).Assembly
kısmı. Burada ne C# kodu ne de Visual Basic koduna değiniyoruz. Hastlayer’ın burada bizden istediği sadece algoritmanın derlenip oluşturduğu .NET Assembly dosyasıdır. Farklı dillerde çalıştırılabilmesine imkan sunan nokta da budur.
Algoritma içerisinde Task temelli bir yapıda paralellik sağladık. Fakat diğer paralellik çeşitleri de bulunmakta:
- Task paralelliği:
ParallelAlgorithm
‘de oluşturduğumuz gibi bir fonksiyonun Task ile encapsulate edilerek ayrı ayrı FPGA çekirdeklerinin içerisinde çalıştırılmasıdır. - Komut seviyesinde paralellik: CPU’lardaki komutların pipeline’a atılarak yürütülmesi buna örnektir. Komutlar ayrı ayrı işlem hatları üzerinde çalıştırarak ilgili donanım birimlerinden maksimum verim elde edilmesi amaçlanır.
- Bit seviyesinde paralellik: 32 bit, 64 bit gibi işlemcilerin böyle adlandırılmalarının sebebi, ilgili bit adedini tek seferde işliyor olmalarıdır. Bu sayede 64 bitlik işlemci, 64 bitlik veri geldiğinde tek seferde bunu işleyecek, 32 bitlik işlemci ise aynı veriyi 2’ye ayırarak işleyip iki iş yapması gerekecektir.
Kodun devamında ise, karşılaştırma yapabilmek için hem FPGA’de hem de CPU’da aynı algoritmayı çalıştırıyoruz.
280 thread’in FPGA’de aynı anda çalıştığı durum yaklaşık 300ms
gibi bir zamanda sonuçlanıyor. Verinin alınıp FPGA’e gönderilmesi ve sonucun geri getirilmesi de 400ms
‘den biraz daha az sürüyor. Aradaki 100ms farkın nedeni USB aracılığıyla FPGA’e veri aktarımının sağlanması olduğunu belirtmekte yarar var. Zira ethernet bağlantısı kullanıldığında veya direkt olarak bilgisayarın PCI Express portuna takılan FPGA kartları kullanıldığında aradaki iletişim kanalından doğan gecikme oldukça düşecektir.
CPU’daki çalışma zamanına baktığımızda 3600ms
gibi arada yaklaşık 10 kat fark olan bir tablo ile karşılaşıyoruz. Ayrıca CPU’nun tükettiği güç ile FPGA’in tükettiği güç arasındaki fark da devasa boyutlarda. Bu sebeple Hastlayer ile birlikte FPGA’in sunduğu paralellik ve hız çok önemli bir hale geliyor.
Özet Olarak
Gündelik hayatımızda yazdığımız kodlar CPU’ya yönelik olduğundan ve genel amaçlı işlerde kullanıldığından performansı FPGA’e göre daha düşük olsa bile kullanıcı açısından çok fazla hissedilmediğinde bir problem teşkil etmiyor. Fakat otonom araç yönetimi gibi görüntü işleme ve buna bağlı görsellerin gerçek zamanlı olarak anlamlandırılması gibi süreçlerde FPGA kullanımı kaçınılmaz hale geliyor. Hastlayer da FPGA programlamayı yazılımcı açısından son derece basit hale getirdiği için geliştirim süreci oldukça kısalıyor.
Daha fazla bilgi için GitHub’daki Hastlayer SDK sayfasını ziyaret edebilir, [email protected] üzerinden geliştiriciler ile iletişime geçebilirsiniz. Hastlayer ve FPGA hakkındaki düşüncelerinizi aşağıdaki yorum kısmında bizimle paylaşabilirsiniz. Bir başka yazıda görüşmek üzere.