Delphiで人工ニューラルネットワーク(Artificial Neural Network)のクラスを作りましたが遅いです

解決


mam  URL  2023-01-11 10:10:43  No: 150739  IP: 192.*.*.*

Delphiのみで人工ニューラルネットワーク(Artificial Neural Network)のクラスを作りました。
DLLなどは不要です。単純な回帰問題や分類問題なら解けるみたいです。
https://mam-mam.net/delphi/ann.html

参考までですが、このクラスを使って、日経平均株価終値を予測してみましが、
やはりダウ平均株価や、その他もろもろの情報も学習させないと、精度が悪いようです。
https://mam-mam.net/delphi/ann_stock.html 

DelphiではGPUは使えないと思いますのでCPU処理になりますが、
処理速度を速くする方法や、精度を上げる方法などがありますでしょうか。

編集 削除
裏目小僧  2023-02-16 21:33:13  No: 150796  IP: 192.*.*.*

こんにちは。今手元に最近のDelphiがなく、動作させられないのです(TArrayはLazarusには実装されているのですが、TStreamのReadDataのような型自由なメソッドとかが無いので)。
高速化したいとすれば
 
function TMamAnn.Sigmoid(x: Single): Single;
begin //シグモイド関数
  result:=1.0/(1.0+system.exp(-x))
end;

function TMamAnn.SigmoidDerivative(x:single): single;
begin //シグモイド関数の微分関数
  result:=Sigmoid(x);
  result:=result*(1.0-result);
end;
  と  RandG 関数くらいでしょうか

たぶん精度は必要なく、そこそこ2割程度の誤差で似た結果が得られれば使い物になるでしょうから

編集 削除
mam  URL  2023-02-17 05:24:12  No: 150797  IP: 192.*.*.*

裏目小僧 様
お返事ありがとうございます。

RandG 関数は初期化時に使っているだけなので、高速化しても全体への影響が殆どないみたいなのを確認しています。
問題となるシグモイドとシグモイドの微分関数(導関数)で使用しているexp関数ですが、Delphiのライブラリ(system.pas)のソースコードを確認しますと、
アセンブラで記述されていまして浮動小数点スタックレジスタとFPUをつかってハードウェア(CPU)で処理を行っていて
FLDL2E  CPU命令( x*log2e がハードウェアで高速に処理)
FXCH CPU命令(2乗処理をハードウェアで高速処理)
などなど、CPU命令(ハードウェア)で処理を行っている為、多少の誤差を無視したプレ計算などの高速化処理をソフトウェアで行ったところで到底速くならなさそうです・・・。

そこで、中間層の最適化関数にSigmoidではなく、ReLUも選択できるようにした改造版を公開しました。
https://mam-mam.net/delphi/ann.html

中間層及び中間層にニューロンが多い場合にはReLUを使用すると処理速度は少し速くなりました。
これ以上速くするにはオンライン学習ではなく、ミニバッチ学習にしてTTaskなどで並列処理を行うなど、根本から学習方式を変えるしかないのかもしれません。

ご意見ありがとうございます。

編集 削除
裏目小僧  2023-02-17 19:17:58  No: 150798  IP: 192.*.*.*

そうですね。 沢山呼ばれている関数を探して、それを高速化するとしても、今のコンパイラは必ずスタックフレームを作るので、それよりもインライン化する方がマシかもしれませんし、それにしても数%といった所でしょう。
細かな高速化としては 
 条件判断をループ内でしないで 関数毎分けてしまうとか
 メソッドにしないで関数にするとか 
 動的配列の配列より2のべき乗サイズの固定配列が少しだけ軽いとか
これらも数%でしかありません。それでよければ来週あたりすこしやってみますが

それよりも並列処理化出来る部分をみつけて並列処理すれば 並列処理分時間は短くなるので何倍も高速になりますからね。
インラインアセンブラでSSE命令を使うとか(もっともLazarusとDelphiでインラインアセンブラの仕様が異なるので厄介ですが)

編集 削除
mam  2023-02-18 01:51:56  No: 150800  IP: 192.*.*.*

裏目小僧 様

レスありがとうございます。

>条件判断をループ内でしないで 関数毎分けてしまうとか
なるほどです。冗長化しますがif文を通る回数がかなり減ります。

>これらも数%でしかありません。
VBのif文は遅い印象がありますが、確かにDelphiのif文は高速動作します。
その他記載の工夫もほんの少しなんですね。

>よければ来週あたりすこしやってみますが
いえいえ、アドバイスだけでも十分有り難いので、これ以上お手数をおかけするわけにはいきません。

>並列処理化出来る部分をみつけて並列処理すれば
やっぱりそうなんですよね。

色々と貴重なアドバイス有難うございます。
素のpytonの50倍以上(numpy使っても10倍以上)の速さでdelphiでは処理できてますので、良しとして妥協するか、うーん。

編集 削除
裏目小僧  2023-02-22 01:56:56  No: 150806  IP: 192.*.*.*

そうですか。時間が出来たので見に来たのですが、余計なお世話似なるので止めておきましょう。
学習が遅いのなら、
学習が何をしてるかというとたぶん確率計算をしてるのでしょう。
ようは主成分分析のような事をして、どの確率が一番高いかを求めているのでしょうね。

だから、学習を早くするなら最初からある程度の方向性を与えてやるというのが早道かもしれません。
ニュートン法とかの解を得るのに近似値を先に与えると高速になるような感じでです。
最初の重みをランダムにするのではなく回転行列を設定しておくとか面白いかもしれませんね。

まあ自分がやらないから勝手な事を言えるのですが

編集 削除
mam  URL  2023-02-22 03:56:37  No: 150807  IP: 192.*.*.*

主成分分析は、私もフリーソフトをdelphiで作って公開しています。(相関分析と主成分分析)
https://mam-mam.net/download/scatterplot/

ちなみに重回帰分析ができるエクセルのアドインも公開しています。
https://mam-mam.net/download/mam_statistics.html

ニューラルネットワークは確率統計学とは全く別のアプローチを行うものになります。(人工的な脳構造の再現ですね。)
重みの初期値を与える時は、正規分布の乱数でで与えるのが最も収束が速い(少ない学習回数で学習できる)らしいので、採用しています。

何れにせよ、貴重なご意見を頂けることは、どんなご意見でもヒントになる可能性が高いのでとても助かります。

編集 削除
裏目小僧  2023-02-22 04:15:43  No: 150808  IP: 192.*.*.*

そうだ SSE系命令で単精度並列命令としては

ADDPS        xmm1, xmm2       xmm1+=xmm2
VADDPS       xmm1, xmm2, xmm3  xmm1=xmm2+xmm3

MULPS        xmm1, xmm2       xmm1*=xmm2
VMULPS       xmm1, xmm2, xmm3  xmm1=xmm2*xmm3
VFMADD231PS  xmm1, xmm2, xmm3  xmm1+=xmm2*xmm3
VFNMADD231PS xmm1, xmm2, xmm3  xmm1-=xmm2*xmm3

Vが付いてるのは AVX系なので一度に単精度8個同時に計算出来ますよ

編集 削除
裏目小僧  URL  2023-02-23 07:30:53  No: 150809  IP: 192.*.*.*

リンク先はSIMD命令の例です Lazarusで試しているのでasmの書き方はDelphi用に直す必要があるかもしれません。一応アセンブラの文法は{$ASMMODE intel}   にしてDelphiに寄せてはいるのですが(win64環境を想定しています)

// for i:=0 to  Count-1 do  arD[i]:=arD[i] + coff*arS[i];
procedure AVXcoffSum(var arD, arS: single; coff: single; Count: integer);
var
  //single32bitを 1ループで32個処理する
  MulData: array [0..7] of single;
  pD, pS: ^single;
begin
  pD := @arD;
  pS := @arS;
  MulData[0] := coff;//値渡しなのでローカル変数にcopy
  while Count >= 32 do
  begin
    asm     //以下は FMA+AVX 命令が使えればOK
             LEA     RCX, MulData[0];
             VBROADCASTSS ymm5, [RCX]  //AVX 256bitレジスタに32bit値を埋める
             MOV     RAX, pS           // ソースアドレスをRAXに
             MOV     RDX, pD           //加算してゆく先

             VMOVUPS YMM0, [RDX]       //AVX
             VMOVUPS YMM1, [RDX+32]
             VMOVUPS YMM2, [RDX+64]
             VMOVUPS YMM3, [RDX+96]

             VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
             VFMADD231PS YMM1, YMM5, [RAX+32]
             VFMADD231PS YMM2, YMM5, [RAX+64]
             VFMADD231PS YMM3, YMM5, [RAX+96]
             VMOVUPS [RDX],    ymm0
             VMOVUPS [RDX+32], ymm1
             VMOVUPS [RDX+64], ymm2
             VMOVUPS [RDX+96], ymm3
    end ['RAX', 'RDX', 'ymm0', 'ymm1', 'ymm2', 'ymm3', 'ymm5'];
    Inc(pD, 32);
    Inc(pS, 32);
    Dec(Count, 32);
  end;
  while Count > 0 do
  begin
    pD^ := pD^ + MulData[0] * pS^;
    Inc(pD);
    Inc(pS);
    Dec(Count, 32);
  end;
end;

編集 削除
mam  2023-02-23 23:59:36  No: 150812  IP: 192.*.*.*

色々とありがとうございます。
SSEは512ビット演算が1クロックサイクルで出来るのですが、先ずは固定4個ずつのsingle配列又はTVec4レコード型などに代入したものをストアしないといけないので、その代入コストや余り処理を考えながら作るのは、私には敷居が高すぎて作れないです。
また、私の使っているDelphiでは
VFMADD231PS
等が未定義の識別子として認識されないですね。
movups xmm0, [a]
は大丈夫なのですが、avx系は全てダメみたいです。
誠に申し訳ございません。

編集 削除
裏目小僧  2023-02-24 05:30:08  No: 150813  IP: 192.*.*.*

TVec4を間に入れる必要は無いでしょう。
まずWin64なら少なくともメモリー確保時に 16byteアラインはされていると思います。それであれば遅くはなっても動くような記述があったような。
そして、それでもパフォーマンスが半減するだけで並列に8個計算できるのですから現状の何倍もの速度は出るはずです。

 32byteアラインされているかどうかは、
https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7x64+SIMD%CC%BF%CE%E1%A4%F2%BB%C8%A4%C3%A4%C6%A4%DF%A4%EB でやってるように
 if (integer(pD) and (32 - 1)) <> 0 then 32byteアラインされてないよ
と確認出来ます(ポインタの下位5bitが0ならアラインされているわけです)
16byteアラインされているかどうかは、このコードで32を16にしてやれば良いわけです

アラインされていなければ、SIMD部をスキップして
  while Count > 0 do
  begin
    pD^ := pD^ + MulData[0] * pS^;
    Inc(pD);
    Inc(pS);
    Dec(Count, 32);
  end;
だけを実行させれば良いでしょうし、この部分だけでもWIN64だと
            movss  -0x40(%rbp),%xmm0
            mulss  (%rax),%xmm0
            addss  (%rdx),%xmm0
            mov    -0x48(%rbp),%rax
            movss  %xmm0,(%rax)
のようにx87FPUではなく SSE命令を使っていますから これを4word並列変更するだけで高速化は出来るのでしょう。
Delphiが32bit版ならごめんなさい。

で、Delphiが64だけど アセンブラが対応してない場合は Lazarus64等のコンパイル出来る環境でコンパイルして そこから命令を直接 DBで書いてゆけばいけます。
例えば VFMADD231PS YMM0, YMM5, [RAX] なら
000000010002F87C c4e255b800               vfmadd231ps (%rax),%ymm5,%ymm0
から最初は64bitアドレス、命令バイトの並びですから c4e255b800      の部分から          
                    DB c4h,e2h,55h,b8h,00h のように書いてやればOK
なおアセンブラでの16進数は c4hではなく 0c4hであったり 0xC4 だったりでDelphiがどうかは判りません 一度 ブレークポイントをかけてCPU窓を見て判断して下さい


以下、該当部分のLazarus-アセンブラ窓(逆アセ表示はlinuxスタイルなので転送方向が逆です)


                            while Count >= 32 do
000000010002F850 eb68                     jmp    0x10002f8ba <AVXCOFFSUM+314>
000000010002F852 660f1f440000             nopw   0x0(%rax,%rax,1)
                             LEA     RCX, MulData[0];
000000010002F858 488d4dc0                 lea    -0x40(%rbp),%rcx
                              VBROADCASTSS ymm5, [RCX]  //AVX 256bitレジスタに32bit値を埋める
000000010002F85C c4e27d1829               vbroadcastss (%rcx),%ymm5
                            MOV     RAX, pS           // ソースアドレスをRAXに
000000010002F861 488b45b0                 mov    -0x50(%rbp),%rax
                              MOV     RDX, pD           //加算してゆく先
000000010002F865 488b55b8                 mov    -0x48(%rbp),%rdx
                              VMOVUPS YMM0, [RDX]       //AVX
000000010002F869 c5fc1002                 vmovups (%rdx),%ymm0
                              VMOVUPS YMM1, [RDX+32]
000000010002F86D c5fc104a20               vmovups 0x20(%rdx),%ymm1
                              VMOVUPS YMM2, [RDX+64]
000000010002F872 c5fc105240               vmovups 0x40(%rdx),%ymm2
                              VMOVUPS YMM3, [RDX+96]
000000010002F877 c5fc105a60               vmovups 0x60(%rdx),%ymm3
                              VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
000000010002F87C c4e255b800               vfmadd231ps (%rax),%ymm5,%ymm0
                              VFMADD231PS YMM1, YMM5, [RAX+32]
000000010002F881 c4e255b84820             vfmadd231ps 0x20(%rax),%ymm5,%ymm1
                              VFMADD231PS YMM2, YMM5, [RAX+64]
000000010002F887 c4e255b85040             vfmadd231ps 0x40(%rax),%ymm5,%ymm2
                              VFMADD231PS YMM3, YMM5, [RAX+96]
000000010002F88D c4e255b85860             vfmadd231ps 0x60(%rax),%ymm5,%ymm3
                              VMOVUPS [RDX],    ymm0
000000010002F893 c5fc1102                 vmovups %ymm0,(%rdx)
                              VMOVUPS [RDX+32], ymm1
000000010002F897 c5fc114a20               vmovups %ymm1,0x20(%rdx)
                              VMOVUPS [RDX+64], ymm2
000000010002F89C c5fc115240               vmovups %ymm2,0x40(%rdx)
                              VMOVUPS [RDX+96], ymm3
000000010002F8A1 c5fc115a60               vmovups %ymm3,0x60(%rdx)
                              Inc(pD, 32);
000000010002F8A6 488145b880000000         addq   $0x80,-0x48(%rbp)
                              Inc(pS, 32);
000000010002F8AE 488145b080000000         addq   $0x80,-0x50(%rbp)
                              Dec(Count, 32);
000000010002F8B6 836de020                 subl   $0x20,-0x20(%rbp)
                              while Count >= 32 do
000000010002F8BA 837de020                 cmpl   $0x20,-0x20(%rbp)
000000010002F8BE 7d98                     jge    0x10002f858 <AVXCOFFSUM+216>
                              while Count > 0 do

編集 削除
裏目小僧  2023-02-24 06:06:42  No: 150814  IP: 192.*.*.*

ちょっとダラダラ書いて訳が判らないかもしれませんが

SSE命令はアライメントを合わせないと例外で落ちるけど
Vの付いた命令は同じ内容の命令(xmmであっても)例外は出ません。

ですからアライメントの心配なく使えます。速度が最速より落ちるだけです。
それでも並列化しないより数倍早くなります。CPUのキャッシュ内なら倍違っても、最終的にはDRAMに読み書きしないといけないから、大量の配列相手だと、それで制限されるんで。

ただCPUの普及状態を考えると FMA+AVX2あたりまでかな
AVX512は私のPCでも使えないし

編集 削除
裏目小僧  2023-02-24 06:21:49  No: 150815  IP: 192.*.*.*

後半のwhile分の中は
× Dec(Count, 32);
〇 Dec(Count, 1);
でした

編集 削除
mam  2023-02-25 09:01:01  No: 150820  IP: 192.*.*.*

裏目小僧さま
よくわかっていないのですが、Delphiの64Bit制約のため以下のように2つの関数にしてみました。
使用しますと以下の■※の個所でACCESS_VIOLATIONエラーになります。
私にはもはや全く分からずです。すいません。

procedure CoffSum(pD,pS:PSingle;MulData:TMulSingle);
asm
  LEA     RCX, MulData[0]
  //VBROADCASTSS ymm5, [RCX]  //AVX 256bitレジスタに32bit値を埋める
  db $c4,$e2,$7d,$18,$29
  MOV     RAX, pS           // ソースアドレスをRAXに
  MOV     RDX, pD           //加算してゆく先
  //VMOVUPS YMM0, [RDX]       //AVX
  db $c5,$fc,$10,$02
  //VMOVUPS YMM1, [RDX+32]
  db $c5,$fc,$10,$4a,$20
  //VMOVUPS YMM2, [RDX+64]
  db $c5,$fc,$10,$52,$40
  //VMOVUPS YMM3, [RDX+96]
  db $c5,$fc,$10,$5a,$60
  //VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
  db $c4,$e2,$55,$b8,$00
  //VFMADD231PS YMM1, YMM5, [RAX+32]
  db $c4,$e2,$55,$b8,$48,$20
  //VFMADD231PS YMM2, YMM5, [RAX+64]
  db $c4,$e2,$55,$b8,$50,$40
  //VFMADD231PS YMM3, YMM5, [RAX+96]
  db $c4,$e2,$55,$b8,$58,$60
  //VMOVUPS [RDX],    ymm0
  db $c5,$fc,$11,$02  ─────────────■※ここでエラーになります
  //VMOVUPS [RDX+32], ymm1
  db $c5,$fc,$11,$4a,$20
  //VMOVUPS [RDX+64], ymm2
  db $c5,$fc,$11,$52,$40
  //VMOVUPS [RDX+96], ymm3
  db $c5,$fc,$11,$5a,$60
end;

// for i:=0 to  Count-1 do  arD[i]:=arD[i] + coff*arS[i];
procedure AVXcoffSum(var arD, arS: single; coff: single; Count: integer);
var
  //single32bitを 1ループで32個処理する
  MulData: TMulSingle;
  pD, pS: PSingle;
begin
  pD := @arD;
  pS := @arS;
  MulData[0] := coff;//値渡しなのでローカル変数にcopy
  while Count >= 32 do
  begin

    CoffSum(pD,pS,MulData);

    Inc(pD, 32);
    Inc(pS, 32);
    Dec(Count, 32);
  end;
  while Count > 0 do
  begin
    pD^ := pD^ + MulData[0] * pS^;
    Inc(pD);
    Inc(pS);
    Dec(Count, 1);
  end;
end;

編集 削除
裏目小僧  2023-02-25 19:23:30  No: 150821  IP: 192.*.*.*

asmをいちなり書くと、アセンブラ関数になります。 その場合、ローカル変数が定義出来ないのでbeginの後にasmを書くインラインアセンブラにした方が良いでしょう。
もう一つの注意は壊してはいけないレジスタは保存しなければいけない事です。
Lazarusの場合は asm end 時にasm内で使用したレジスタを報告するので勝手にやってくれますが、Delphiはそうではなかったように記憶しています。
https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3_%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA_%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E4%BD%BF%E7%94%A8
壊してはいけないレジスタに XMM5 があるので ymm5を使っているので XMM5を保存しなければいけません。
https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA%E3%81%AE%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0
.SAVENV XMM5 が先頭に必要になるのでしょう
一度 ブレークポイントで止めて CPU迄で配置されたアセンブラコードを見るべきです。

例外で止まってしまう原因ですが、アセンブラ関数としていきなり引数を渡している点になると思われます。
上のリンク先にもありますが 64bitの引数は RCX、RDX、R8、 R9 という並びになります。
https://learn.microsoft.com/ja-jp/cpp/build/x64-calling-convention
よって

procedure CoffSum(pD,pS:PSingle;MulData:TMulSingle);
RCX pD,
RDX   pS
R8     MulData
 となっているはずです。 TMulSingleが配列なら MulDataは配列へのポインタです
よって 最初のアセンブラ行
   LEA     RCX, MulData[0]
により pDの値が上書きされていると思われます。
ブレークポイントで止めてCPU窓でかくにんしてください
  MOV     RDX, pD           //加算してゆく先

  MOV     RDX, RCX          
となっていませんか?
その場合、 RDXは RCXつまりMulDataに化けていて MulDataはポインタなので呼び出し元のMulData配列に書き込んでいるのしょう。MulDataが arra[0..31] of doubleでないなら、エラーになると思います。

対策は、私のコードのように、いちどローカル変数を定義して、そこに引数をcopyしてやるかprocedure の引数の順番を変えてやる事です。
元のようにループ内をインラインアセンブラにした方が良さそうに思えます。
その場合はxmm5が保存されているかCPU窓で確認してください。
保存されていないならarray [0..3]of singleの領域に xmm5を保存し復元して下さい VMOVUPS は無くても MOVUPSは使える筈(64bitなら)

なお、
  MulData: array [0..7] of single;
としたのは VMOVUPS  でcoffを渡すために 8個同じ値を入れようと思ったからで  VBROADCASTSS  という便利命令を使ったので 配列にする必要は後から考えると無かったのです。

編集 削除
裏目小僧  2023-02-25 19:43:40  No: 150822  IP: 192.*.*.*

あ、xmm5 を保存するために MOVUPSを使う場合、領域は16byteアラインされていないといけません。
ローカル変数の宣言順でアラインが崩れるとVMOVUPSと違って例外が出ます。
そのためにwin64ではスタックフレームや動的確保でも16byteアラインを強制されますか
例外が出たら他の変数宣言を1個だけ入れ替えるか
array [0..7] of single; で32byte分確保して
p:Integerに代入して p:=(p+8) and -16;として pをRAXとかに代入してやれば16byteアラインされます

お勧めは例外が出たら入れ替える方針です。一度順番が決まってしまえばOKですからね

編集 削除
裏目小僧  2023-02-25 20:09:03  No: 150823  IP: 192.*.*.*

いけない。MOVUPS はアライメントが崩れても例外は出ない MOVDQUもそうだった。
アライメントが必要なのは movDQA 
つまりMOVUPSなら保存時にアラインは考えなくていい。
それから保存なら浮動小数点のMOVUPSより 整数のMOVDQUが安全

スタックへの保存や復元なら
sub rsp,16
movdqu [rsp],xmm5
とループ外で保存してから
 ループ内コードを書いて
復元は
movdqu xmm5,[rsp]
add rsp,16
の方がいいかな。ローカル変数定義しなくていいし

編集 削除
mam  2023-02-25 23:44:48  No: 150824  IP: 192.*.*.*

裏目小僧 様

Delphiの64ビットは制約があり、
procedure hoge();
begin
  asm
  end;
end;
は記述できません。
procedure hoge();
asm
end;
のみになります。
32ビットなら可能なのですが・・・。

編集 削除
裏目小僧  2023-02-26 01:31:36  No: 150825  IP: 192.*.*.*

なるほど。それは面倒ですね。
なら、とりあえずAVX命令は諦めてSSE命令で並列化してみては?
こんな感じです。
2つのアセンブラ関数は関数内関数にするとレジスタの先頭に隠れたポインタが渡されるので関数の外にある必要があります。
これで高速化するなら AVXでさらに高速化出来るぞと.
2つにしたのは callのオーバーヘッドが大きいので4並列を3つ関数内で書いて12回分としてcall回数を減らそうという事です。
さらに長く続けても速くなるなら問題ないですよ。




procedure coffSum4(var pD, pS, coff4: single);
 asm  //               RCX  RDX
          MOV     RAX,coff4
          MOVUPS  XMM0, [RAX]      // coffは
          MOVUPS  XMM1, [RDX]       //4word一度に処理する
          MOVUPS  XMM2, [RCX]       //4word一度に処理する
          MULPS   XMM1, XMM0        //coff*pS^
          ADDPS   XMM2, XMM1
          MOVUPS  [RCX],XMM2
 end;
procedure coffSum12(var pD, pS, coff4: single);
 asm  //               RCX  RDX  R8
          MOV     RAX,coff4
          MOVUPS  XMM0, [RAX]      // coffは
          MOVUPS  XMM1, [RDX]       //4word一度に処理する
          MOVUPS  XMM2, [RCX]       //4word一度に処理する
          MULPS   XMM1, XMM0        //coff*pS^
          ADDPS   XMM2, XMM1
          MOVUPS  [RCX],XMM2
          MOVUPS  XMM1, [RDX+16]       //4word一度に処理する
          MOVUPS  XMM2, [RCX+16]       //4word一度に処理する
          MULPS   XMM1, XMM0        //coff*pS^
          ADDPS   XMM2, XMM1
          MOVUPS  [RCX+16],XMM2
          MOVUPS  XMM1, [RDX+32]       //4word一度に処理する
          MOVUPS  XMM2, [RCX+32]       //4word一度に処理する
          MULPS   XMM1, XMM0        //coff*pS^
          ADDPS   XMM2, XMM1
          MOVUPS  [RCX+32],XMM2
end;
//                        RCX  RDX               xmm2         R9
procedure AVXcoffSum(var arD, arS: single; const coff: single; Count: integer);
var  Coff4: array[0..3] of single; //SSE命令の為の4word(16byte)の入れ物
  pD, pS: ^single;
  i: integer;
begin
  pD := @arD;//レジスタで
  pS := @arS;
  for i := 0 to High(Coff4) do coff4[i] := coff; // shufps にしてくれるかな
    while Count >= 12 do   //SSE命令はWin64なら必須だから4個一度に計算可能
    begin
      coffSum12(pD^, pS^,coff4[0]);
      Inc(pD, 12);
      Inc(pS, 12);
      Dec(Count, 12);
    end;
  while Count >= 4 do   //SSE命令はWin64なら必須だから4個一度に計算可能
  begin
    coffSum4(pD^, pS^,coff4[0]);
    Inc(pD, 4);
    Inc(pS, 4);
    Dec(Count, 4);
  end;

  while Count > 0 do   //残りは最大3個だから
  begin
    pD^ := pD^ + coff * pS^;
    Inc(pD);
    Inc(pS);
    Dec(Count, 1);
  end;
end; 

編集 削除
裏目小僧  2023-02-26 03:03:34  No: 150826  IP: 192.*.*.*

SSE並列化命令を使っても思った程早くならない場合次はキャッシュを意識したコードにしなければいけません。
たとえばFFTのような処理なら1層づつ回転変換を処理してゆくと、キャッシュサイズを超えるととたんに遅くなります
その場合、キャッシュ内のデータが書きだされる前にそれを使う上層に使わせるという工夫が必要になります。
もちろんFFTはブロック同士が交差しますから上の層は下の層の半分しか処理できませんが

私のpcのキャッシュは3Mバイトですから 単精度なら複素数30万個あたりで分割する必要が出てきますね。
キャッシュサイズもCPUIDで調べる事が出来るので、高速化を考えるならそれを取得してやるという事になるでしょう。

その次の壁はメモリーサイズです。PCに内臓してるDRAMの範囲を超えると、これはもうどうしようもありません。
仮想記憶でHDDが使われるようになると、それがネックとなりSSEとかAVXとか無意味になってきます。

まあデータを8BITにしてバイトで保持してやれば1/4にはなります。

大昔、8BITで対数的に保持して掛け算 足し算はテーブル引きで処理した事はありますが
まあそれでも、そこそこの結果が得られたように覚えています。
当時は掛け算さえ遅かった時代なので 速度面で使った手法であり。今では無意味ですが
メモリ面では見直される手法かもしれませんね。
計算速度よりもメモリ消費量が速度を決める領域なれば有効でしょう。

学習さえ出来ればokで計算精度とか無意味な領域のように思えるので

編集 削除
mam  2023-02-26 07:13:53  No: 150827  IP: 192.*.*.*

裏目小僧様

for i:=0 to  Count-1 do  arD[i]:=arD[i] + coff*arS[i];
の処理を行っていて、繰り返しが多くて簡単に置き換え可能な箇所はBackProp関数(逆伝播)に2か所あるので置き換えてみました。
(AMD Ryzen3 3200G 3.60GHz 4C L1キャッシュ384KB L2キャッシュ2MB L3キャッシュ4MB、RAM:24.0GB)

排他論理和のNN学習で1万回の繰り返し学習、
入力層2ニューロン、中間層128ニューロン、中間層32ニューロン、出力層1として学習個所だけをTStopWatchで処理時間を測って比較してみました。
アセンブラに置き換えない(64ビットコンパイル)の場合の処理時間は2.821秒でした。
アセンブラに置き換えた(64ビットコンパイル)場合は2.218秒でした。

入力層2ニューロン、中間層128ニューロン、中間層64ニューロン、出力層1の場合、
アセンブラに置き換えない(64ビットコンパイル)の場合の処理時間は5.121秒でした。
アセンブラに置き換えた(64ビットコンパイル)場合は4.045秒でした。

アセンブラに置き換えたほうが20%程度速くなっています。

編集 削除
裏目小僧  URL  2023-02-26 07:36:21  No: 150828  IP: 192.*.*.*

お疲れ様でした。
win64はSSE命令で浮動小数点を扱う(といっても並列ではなく単項で)ので、並列化すれば計算部だけは並列具合だけ高速になる筈。つまり
積和部分だけに限れば4倍高速になる筈なので
A もともとDelphiがある程度並列演算をしていた(Lazarusはしてくれない)
B 置き換えられる部分が全体から見て少なかった
C キャッシュから外れる程メモリを食っていた
という感じでしょうか

2割程度の改善だと さらに FMA+AVXを使っても大きな改善は望めませんね。
積和以外の部分で並列化可能な部分を探すしかないでしょう。

編集 削除
裏目小僧  URL  2023-02-26 22:51:23  No: 150831  IP: 192.*.*.*

https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7RDTSC%A4%F2%BB%C8%A4%A6}
私の方でもfor文 AVX SSEで処理時間の差を見てみました.256個のテストです
結果は結構バラツクのですが
こんな感じです。 AVX命令が方が遅いのかと思いましたが
no,       cyc,全体の,st回数,ed回数,ed/st ,"name"
 0,      2258, 51.09,     1,     1,100.00,"for文による場合"
 1,       844, 19.10,     1,     1,100.00,"AVXによる場合"
 2,       620, 14.03,     1,     1,100.00,"SSEによる場合"
  ,      4420cyc=0.00ms このCPUは1msで2326315.8cyc実行

試験する順番を入れ替えると
no,       cyc,全体の,st回数,ed回数,ed/st ,"name"
 0,      2556, 55.52,     1,     1,100.00,"for文による場合"
 1,       366,  7.95,     1,     1,100.00,"AVXによる場合"
 2,       864, 18.77,     1,     1,100.00,"SSEによる場合"
  ,      4604cyc=0.00ms このCPUは1msで2423157.9cyc実行

命令キャッシュも影響してるのかもしれませんね

編集 削除
mam  2023-02-27 05:30:28  No: 150832  IP: 192.*.*.*

裏目小僧様
掲示して頂いたアセンブラで、他に適用出来そうな部分を探して試してみたのですが、速度はあまり変わらなかった(そもそもループ内で多く使われてないところなので当然なのですが)です。オンライン処理からミニバッチ処理にして並列化しかなさそうな気がしています。でもミニバッチ処理だと使い勝手が少し悪くなるのと、メモリー消費が大きくなるので、これで妥協します。
Delphiの64ビットアセンブラの制限も痛いです…(日本語のフォルダを使うとCPUウィンドウが表示出来ない、プロシージャ内にdelphiとアセンブラの両方を記述できない、XE11以降しかAVX命令に対応していないためハンドアセンブルでdbで機械語記述)

色々とありがとうございます。

編集 削除
mam  2023-02-28 08:35:08  No: 150834  IP: 192.*.*.*

裏目小僧様
とてもお世話になっていましてありがとうございます。
掲載頂いたアセンブラsseで、25%、場合によっては35%速くなりますので、私のホームページにソースコードを掲載させて頂いても宜しいでしょうか。
厚かましいお願いですいません。

編集 削除
裏目小僧  URL  2023-03-02 21:41:19  No: 150852  IP: 192.*.*.*

ソースの公開、問題ありませんよ。 誰でも使って下さいというスタンスですから。

あれから私の方でもDelphi5で使ってみたくて SSE/AVX命令をDB分にして動かせるようにしました。 Win64でのDelphiでのテストは出来ていませんが、Lazarus64では動いています。

ここを見た人で欲しい人が出るかもしれないので、リンクを掲載しますね。
https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7%BA%EE%A4%C3%A4%BFSIMD%B4%D8%BF%F4%A4%F2Delphi%A4%C7%BB%C8%A4%A6
このページの  AVX_func_delphi.inc にあります


編集 削除
mam  2023-03-07 11:30:58  No: 150874  IP: 192.*.*.*

裏目小僧様

いろいろとありがとうございました。

編集 削除