誰かやねうら王miniのデバッグきぼんぬ

やねうら王mini、さすがに弱すぎておかしい。
試しにやねうら王2015と対戦させたら、100局やって全敗した。

おかしすぎ。

少し弱い程度ならわかるが、全敗はさすがにおかしい。

間違いなくどこか重要なところがバグっている。

真っ先に評価関数を疑ったが、いくつかの局面において調べたが、評価値はやねうら王2015とぴったり一致する。(ようだ)

次に評価値の差分計算を疑ったが、これも全計算と比較するassertを有効にしたが、そこでは引っかからない。評価値の計算も正しい。(ようだ)

前回から新たに起こしたコードが怪しいと思って、1手詰めを疑ってみたが、1手詰めは有効にすると自己対戦時の勝率は上がるので、どうもそこではなさそうだ。

となると一番怪しいのは置換表絡みだ。

昔のStockfish風のコードを書いてみた。

そうすると一つ前のminiに5%ぐらいしか勝てなくなった。うおおおい!
何故この置換表で駄目なのかがわからん。5分ぐらい考えたがわからん。(もっと考えろよ!というツッコミはご容赦願いたい。)

その腐った実装を含めてGitHubのほうに上げといた。forkしてコードを書くべきだと思うが、老害の俺様そんな技は知らん。

ソースコード : V2.15デバッグきぼんぬ https://github.com/yaneurao/YaneuraOu/tree/b222677897d4dfbe1139946d60e7bd752af2809e

shogi.h
#define YANEURAOU_CLASSIC_ENGINE // やねうら王classic (開発中)
を有効にして、
extra/config.h
の先頭にある
#define NEW_TT
をundefして、これをundefする前のものと比較して原因を調査して欲しい。

本当は、上のバグの原因とは別に、内在しているであろう致命的なバグのほうも発見して欲しいが、そこまでは望まない。

なぜブログでこんなことをお願いしているかというと、今日の12時からCODE VS 5.0が始まるのだ。
なぜだかは知らないが、俺様、これに参加して上位入賞しないと俺様の威厳が保てないらしいのだ。

というか、「ブログでデバッグきぼんぬ」とか書いている時点で威厳もへったくれもないような気もするが、CODE VS 5.0の終了日まで他の予定も結構詰まっていて、もう何もかも手詰まりだ。

そんなわけで、やねうら王mini、上に書いた致命的なバグをとるとたぶんR200~300ぐらい上がるはずなんだ。だから助けてくれ…。お前らだけが頼りだ。


誰かやねうら王miniのデバッグきぼんぬ” への49件のコメント

  1. やねうら王nanoの時は
    make_move

    unmake_move
    で駒を取った時に戻す駒の先手後手がバグってたの、
    同じことが残っているってことはないんですか?
    generate_moveは読んだんですが
    どこが事件現場の駒取り、戻しか分りませんでした

    • さすがにdo_move/undo_moveがバグっていれば、探索したときにすぐに盤面がおかしくなるのでどこかのassertに引っかかるはず。generate_move()の生成頻度の低い指し手(王手となる指し手など)は、デバッグが十分ではないのでそのへんでバグっている可能性はなくはないです。

      • undo_move
        の駒取りで
        make_pieceの引数に~Us
        が渡されているんですが、
        Usはenumの0と1なので
        チルダでひっくり返るのでしょうか?
        このへんC++の仕様をよく知らないです。
        booleanなら0,1なのは分りますが。
        読んでみて気になったのはそこだけです。

        • やねうら王miniでも当該箇所は同じですね。
          miniで動作が保証されているならここじゃないか・・・。

          • 「miniでも同じですね」って、デバッグきぼんぬもminiですね。ええい!nanoとかminiとか紛らわしい!
            そのうちやねうら王touchになってやねうらPhoneになるんでしょうか。
            ウチのパソコンMacなのでブラウザからコード読んで怪しいと感じたのはそこです。
            position.cpp 1161行です。
            ~Usを~Us&1にすると直りそうな気がするんですが。
            試せないのでそれ以上は言えませぬ。

            連投失礼します。

          • Visual Studio 2015で「~Us」のところにカーソルを持って行って[F1]キーをどうぞ。

            そうすると、あら不思議。その定義のところが表示されます。以下のところですね。

            // 相手番を返す
            constexpr Color operator ~(Color c) { return (Color)(c ^ 1); }

  2. 置換表の件ではないのですが、
    classic_search.cppのqsearch()とsearch()の冒頭に
    pos.check_info_update()の呼び出しを追加してみましたら
    多少勝率があがったように見えます。(誤差の範囲かもしれませんが...)

    不勉強のためソースコードをあまり理解できていないのですが、
    例えばpos.mate1ply()は内部で state()->checkInfo.pinned を使っていますので
    事前にpos.check_info_update()が必要、ということはありませんでしょうか?

    また、insert_pv_in_tt()に
    ASSERT_LV3(MoveList1<LEGAL_ALL>(pos).contains(m));
    がありますが、これも同様に
    事前にpos.check_info_update()が必要、ということはありませんでしょうか?

    もし見当外れでしたらすいません。

    • 調査のほう、ありがとうございます。いまCODEVS5が忙しくて、予選期間終わるまでじっくり確認できそうにないですが、簡単に。

      1. LEGAL_ALLのときは、それだけ指し手生成のほうでcheck_info_update()をやっているという謎実装でして…。

      2. mate1ply()は確かに事前にcheck_info_update()が必要っぽい気がします。ただ、一手詰めあってもなくても勝率ほぼ変わらずなのでそこが根本的な原因ではない気がします。

      3. qsearch/searchのほうは、MovePickerの直前にcheck_info_update()はあるので、これでたぶん十分な気がします。

      その他。

      4. バグをとると勝率が大幅に上がるはずなのです…。誤差どころではなく。
      5. 古いほうの置換表、Depthとかの型変換でどこか符号が吹き飛んでいるとか、その手のバグかも知れないのですが、どうも目視ではバグが発見できず。

      • 早速のご返信ありがとうございます。

        > 1. LEGAL_ALLのときは(以下略)
        なるほど。generateMoves()の中でcheck_info_update()を呼んでいたのですね。

        > 4. バグをとると勝率が大幅に上がるはず(以下略)
        一晩対戦させておきましたら、今度は負け越していました...
        誤差どころか逆効果だったかもしれません。

        置換表の方をもう少し調べてみます。

          • 本題とはあまり関係ないのですが、打ち歩詰め判定(legal_drop())で
            バグらしきものを見つけましたので、ご報告いたします。

            下記局面で先手の9ニ歩打は打ち歩詰めではありませんが、
            legal_drop()では打ち歩詰めと判定されてしまうようです。

            後手の9三の金は先手の9四の飛車によりpinされてはいるものの、
            9ニ歩打には同金と取れるのですが、これが考慮されていないように思います。
            (pinされている方向への移動はOK)

            本来は 9ニ歩打→同金→同飛成 で先手の勝ちですが、
            9ニ歩打を逃すとほぼ先手の負けになります。(9三飛成は同桂が逆王手で、そのまま詰まされる)
            しかし、これはレアケースだと思いますので、おそらく勝率にはあまり影響はないと思います...

            後手の持駒:角二 金三 銀四 桂三 香四 歩十七
            9 8 7 6 5 4 3 2 1
            +—————————+
            |v王v桂 ・ ・ ・ ・ ・ ・ ・|一
            | ・ ・ 飛 ・ ・ ・ ・ ・ ・|二
            |v金 ・ ・ ・ ・ ・ ・ ・ ・|三
            | 飛 ・ ・ ・ ・ ・ ・ ・ ・|四
            | ・ 王 ・ ・ ・ ・ ・ ・ ・|五
            | ・ ・ ・ ・ ・ ・ ・ ・ ・|六
            | ・ ・ ・ ・ ・ ・ ・ ・ ・|七
            | ・ ・ ・ ・ ・ ・ ・ ・ ・|八
            | ・ ・ ・ ・ ・ ・ ・ ・ ・|九
            +—————————+
            先手の持駒:歩
            先手番
            sfen kn7/2R6/g8/R8/1K7/9/9/9/9 b P17p4l3n4s3g2b 1

          • ご報告ありがとうございます!そのバグは今日の昼ごろのGitHubへのコミットで修正されているかと思います。(ちゃんと動作確認はしていないのですが…) (`・ω・´)ゞ

          • 失礼いたしました。打ち歩詰めの件、ちょうど今日修正されていたのですね...
            GitHubの最新版のソースで動作確認しましたところ、下記局面は正しく判定されるようになっていました。

      • pos.check_info_update()の件、修正ありがとうございます。
        なお、classicでは静止探索の方は修正済みでしたが、通常探索の方は未修正のようです。
        (勝率への影響は不明ですが...)

        • V2.23で修正しました(`・ω・´)ゞ

          NULL MOVEのときもCheckInfoのupdateが必要なのを忘れていたのでそこも修正したのですが強さがほとんど変わりません…。うーむ、、どうなっているのだ…。

          CheckInfoが絡む局面が少なすぎて影響が微差なのでしょうか…。

  3. やねさんがバグフィックスすると、そのあとでその部分の(バグフィックス部分の)バグレポートが飛び込んでくる。
    あるいは、なんとかCHでバグレポートされると、それを見る前にやねさんがそのバグをフィックスしてしまう。
    ・・・・・
    このようなことは、私にはとても不思議な事のように思えます。

    • 打ち歩詰めの件は、tanuki-さんが最初に発見して、ツイートしていたので、(報告と修正タイミングが重なったのは)もしかするとそれでかも?

  4. もう少し細かく調べて結果を上げる予定ですが、時間がかかりそうなので先に簡潔に書いておきます。

    ・ハッシュキーが48bit
    ・置換表サイズ 1GB(デフォルト)
    ・benchコマンドを実行した時に2番めの局面

    のときに、Promotionに追加した、歩、香、桂を先後で取り違う違法手が12回(歩、香8回、桂馬4回)発生していて、それが探索を遅延させているように見えます。
    (16/32bitだと16秒、48bitだと40秒)

    ・ハッシュキーのサイズを16や32にする
    ・置換表サイズを128MBにする

    などを行うと違法手が(上書きされて?)発生しなくなります。置換表のサイズを128MB/1024MB/4096MBのときに違法手が発生しないパターンは以下のとおりでした。

    48bit 128MB
    32bit なし
    16bit 1024MB or 4096MB

    他にもあるのですが、ひとまずご報告します。

    • 「古い置換表のバグ修正」のコミットで試したところ、48bitでの衝突回数が減ってパフォーマンスが大幅に上がりました。(40秒->10秒)。もう少し調べたほうが良さそうです・・・

      • 古い置換表のバグ修正は巨大メモリ確保のときの修正だけなので関係ないと思いますよ。
        コメントに4GBと書いてしまいましたが、よく考えたら32bitで表せる範囲×sizeof(TTCluster)なので実際は…。

  5. classic_search.cpp内のTT.probe()とtte->save()(あるいはTT.store())の前後にデバッグログを入れて
    将棋所で適当な局面で10秒ほど「検討」を実行し、新旧置換表でログを比較してみました。
    不勉強のため置換表についてあまり理解できていないのですが、とりあえず気づいたことをお伝えいたします。
    もし見当外れでしたら無視してください...

    (1)probe結果のtte->depth()が新旧置換表で異なることがある。

    【新置換表の場合】
    << [静止探索]probe後:depth=-2, ttDepth=-2, posKey=10127486481804211217, ttHit=true, pretty(tte->move())=3八飛打, tte->value()=31999, tte->depth()=0

    【旧置換表の場合】
    << [静止探索]probe後:depth=-2, ttDepth=-2, posKey=10127486481804211217, ttHit=true, pretty(tte->move())=3八飛打, tte->value()=31999, tte->depth()=256

    1手詰め判定された際にsaveしたDEPTH_MAXをprobeした結果だと思いますが、
    0(新置換表)と256(旧置換表)で異なるようです。(これは旧置換表の方が正しいような気がしますが)

    (2)旧置換表で、saveしていないposKeyをprobeしてしまうことが(結構頻繁に)ある。

    【新置換表の場合】
    << [静止探索]probe後:depth=0, ttDepth=0, posKey=8504931743133269752, ttHit=false

    【旧置換表の場合】
    << [静止探索]fails highのtte->saveの前:posKey=8504931743133269753, value=1471, ss->ply=4, value_to_tt(value, ss->ply)=1471, ttDepth=-2, pretty(move)=8二7一, ss->staticEval=-13
    << (中略)
    << [静止探索]probe後:depth=0, ttDepth=0, posKey=8504931743133269752, ttHit=true, pretty(tte->move())=8二7一, tte->value()=1471, tte->depth()=-2

    旧置換表では局面のハッシュキーの上位32bit(48bit?)しか見ないので
    ある程度の衝突は仕方ないと思いますが、
    数秒間探索しただけで何か所も衝突しているように見えます。
    直近でsaveされたposKeyと1だけ異なり衝突しているケースが多いようです。

    (3)たまに、posの手番とsfenの末尾の手数が合わないことがある。

    << [静止探索]開始
    << pos=
    << ^玉^桂^金 □ □ □ □^桂^香
    << ^香^銀 □^金 □ □ □^角 □
    << ^歩^歩^歩^歩^歩 □ □ □ □
    << □ □ □ □ □^歩^銀^飛 □
    << 歩 歩 □ □ □ □ □ □^歩
    << □ □ 歩 歩 銀 歩 歩^歩 □
    << □ □ 角 □ 歩 金 銀 □ 歩
    << □ 飛 □ □ □ 金 玉 歩 □
    << 香 桂 □ □ □ □ □ 桂 香
    << 先手 手駒 : , 後手 手駒 : 歩
    << 手番 = 後手
    << sfen kng4nl/ls1g3b1/ppppp4/5psr1/PP6p/2PPSPPp1/2B1PGS1P/1R3GKP1/LN5NL w p 63

    これは、新旧置換表の両方で発生しているようです。

    とりあえず、ご報告まで。

    • うおー!!マジ感謝!!tさんにもお会いする機会があれば是非焼き肉おごらせてください!CODE VS 5.0終わったら、速攻直します!!

      旧置換表のコード、いまちらっと見たら
      mem = calloc( … )のところ、sizeof(TTEntry[ClusterSize])でなくていいのかな…。あるいはその上のsizeof(TTEntry[ClusterSize])のところがsizeof(TTEntry)のような…。まあ、確保されるメモリ量がおかしいだけなので弱くなる原因とは関係なさそうですけども。

      • もう既にやねさんは修正方法までお見通し済みかもしれませんが、一応自分なりに、古い置換表が弱い理由を考えてみました。

        ハッシュキーのうち手番は下位1ビットだが、古い置換表はprobe時に上位ビットしかみないので、例えば枝狩りのnull moveでキーが衝突する。盤面と手駒が同じで先後を取り違えた評価値や指し手がprobeされるので、その後の探索がおかしくなる。新置換表は下位ビットも少し見ているので大丈夫。
        すいません。いま出先なので少し端折りました。

        • null moveのキーが衝突するのは罪深いですね!!古いほうの置換表で弱くなる原因、それですか…。な、、なるほど。

          新しいほうの置換表で旧やねうら王より弱い原因が全く思いつかないですが、誰か(特にtさんかwoodyringさんか)旧やねうら王のソースコード渡すのでデバッグきぼんぬ…。

          • まだ自己対戦などしてないので不確かですが、もう1箇所怪しい箇所を見つけました。

            TT.refresh(set_generation)を掛けるタイミングです。

            今のやねうら王の旧置換表では見つけた直後(probe中)にやっていますが、AperyやStockfishではサーチ側で深さやタイプの条件に合致したもののみrefreshをかけます。

            試しにTT.refreshをprobe直後のifの後にいれたところ、benchコマンドで5%ほど置換が多く発生し、ハッシュ衝突による違法手も出なくなりました。ttValue, depthの条件が違法手をカットしてくれているように思えます。

            ただ、この修正で探索深さが浅くなってしまうのでレーティング的には新置換表のほうがよさそうです。

          • TT.refresh()はなんか本家Stockfishのほうでも何度かタイミングが変わってて、色々バリエーションがあるようです。(たぶん例によって10万回とか対戦させると差が出るんだと思いますけども…)

    • > 1手詰め判定された際にsaveしたDEPTH_MAXをprobeした結果だと思いますが、0(新置換表)と256(旧置換表)で異なるようです。(これは旧置換表の方が正しいような気がしますが)

      DEPTH_MAXが書き出せない件、V2.20で修正しました(`・ω・´)ゞ

  6. 上記の変更入れて一晩放置したところ、違法手が出なくなる代わりに明らかに弱くなりました。(1-10ぐらい)
    感覚的にもっともらしい方だけ残すほうが良いような気がしたのですが・・・少し落ち着いて調べてみます。

    • うおー!根が深そうですが、パズル解くみたいで、なんだか楽しそうですね!私もCODE VSの予選が終われば謎解きに加わりますね。

  7. やねうら王2015はMoveは16bitなのですか?
    もしそうでないのだとしたら、miniがあまり強くない理由はMoveが16bitだからかもしれません。
    Moveを16bitにしていると、pseudo_legalでfromにある駒とMoveに格納されている駒種が一致しているかどうか確認できないことが関係しているのではないかと思います。
    私も将棋ソフトを作っていますが、Moveは32bitにしていて、適当な局面を探索させたとき、pseudo_legal相当の関数の呼び出し回数の5%くらいは上記の条件に引っかかるようです。この条件を判定できないと、無意味なキラー手や置換表の手を読まなくてはいけない分、損をしているのではないでしょうか。

    • 確かになるほどなのですが、やねうら王(2014,2015)でもMoveは16bitです。そのほうが指し手生成が速くていいかと思いまして。そんなわけで今回、弱い原因とは無関係の模様です。

  8. 勝率にどの程度影響があるかは分からないのですが、
    CounterMoveHistoryStatsの更新と取得が期待通りに動作していないように思います。

    move_pickerのscore_quiets()や通常探索でMovePickerを呼んだ後の枝刈りなど
    cmhからValueを取得している箇所にデバッグログを入れてみたのですが、常にゼロが取得されていました。
    これに対して、MoveStatsとHistoryStatsは期待通りに動作しているように見えます。

    cmh.updateでそのcmh自体は更新されるのですが、本体のCounterMoveHistoryの方は更新されていないようで、
    おそらく、CounterMoveHistoryStatsの場合は、TではなくT*を返すようなgetterが必要かと思うのですが、いかがでしょうか...?

    • うおおおおお!!!勝率ダウンの原因、まさにそれじゃないですか!!!???原因調査、本当にありがとうございます。

      CODEVS5.0の作業が終わり次第、やねうら王miniのデバッグ作業に戻りますのでよろしくお願いします。

  9. 度々失礼します。
    see.cppのmin_attacker()内で引数const Bitboard(&bb)[8][2]を参照している箇所で
    例えば
     // 斜め方向なら斜め方向の升をスキャンしてその上にある角・馬を足す
     attackers |= bishopEffect(to, occupied) & (bb[BLACK][PIECE_TYPE_BITBOARD_BISHOP] | bb[WHITE][PIECE_TYPE_BITBOARD_BISHOP]);
    のようになっていますが、
     (誤)bb[BLACK][PIECE_TYPE_BITBOARD_BISHOP]
     (正)bb[PIECE_TYPE_BITBOARD_BISHOP][BLACK]
    ではないでしょうか?

  10. 評価値の全計算と差分計算の頻度についてご教示ください。
    1手1秒で対戦させてカウントを取ってみましたところ、
    evaluate(評価関数)の実行回数が1局で約900万回のとき、
    内訳は概ね以下のとおりでした。
     ・compute_eval(全計算)が600万回
     ・calc_diff_kpp()で「if (prev->sumKKP == VALUE_NONE)」ではない場合(差分計算)が300万回

    予想外に全計算が多い印象なのですが、差分計算で遡るのが1手だけの場合はこのくらいが妥当なのでしょうか?
    それとも、何かしらバグが潜んでいる可能性を疑ってみた方がよろしいのでしょうか?

    ※do_move()の実行回数もカウントしてみると1局で約1900万回でした。
    1900万回のうちevaluate()の実行が900万回なので、遡るのが1手だけの場合は差分計算できないことの方が多くても
    それほど不思議ではない、ということになるのでしょうか...?

    • 差分計算少なすぎですね。玉が移動するケースはそんなに多くないので、本来は8割ぐらいは差分計算できないといけないのですけども、静止探索で、王手がかかっているときにevaluateを呼び出さずに次のnodeに行くのでその関係で差分計算が出来ないケースがあるのだと思いますが、それにしてもおかしいですね…。あとで詳しく調べてみます。

      • aperyのようにdomove毎に一回evaluateを呼び出す必要があるのでは?置換表から静的評価値を取り出した時は呼び出されないので。

        • うおおお。

          if (pos.state()->sumKKP == VALUE_NONE)
          evaluate(pos);

          を追加したところ、590knps→733knps/thread になりました。(V2.24)

          やねうら王2015のほう、1Mnpsぐらい出てたはずなのでまだ何かやらかしている可能性が微レ存…。

          • これはバグでも何でもないのですが、evaluateを毎node呼び出すのなら、置換表のevalって削れませんか?
            すると1エントリー10byteが8byteななってキリのいい数字に…

          • 一応、evalの値で枝刈りできるケースがあり、その場合は、置換表のstaticEvalの値を信じて枝刈りして、結局evaluate()は呼び出さないので、このケースがあるので置換表にはstaticEvalは書き出されているほうがお得かなと。[要実験]

            それとは別に、置換表にMoveは32bitで書き出したいという気もしていて、そうすると10byteでなく12byteのエントリーになりそうな気も..。このときcluster sizeを5にして、5×12bytes = 60bytes <= 64bytesみたいに考えてるんですけど。[要実験]

          • なるほど、確かにそういうケースはありますね。
            Moveの件はそれでどれくらい変わるのか興味深いですね。実験結果楽しみにしています。
            また全然関係ないのですが、置換表にKKP,KPP値を登録したらevaluate呼ぶ回数が減らせるかと思い実験してみたのですが、確かにNPSはあがるのですが、置換表の1エントリーが大きくなり,
            cluster sizeが減ってしまって置換表にヒットしなくなる局面が増えて微妙な感じでした。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です