読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

AI PC調査

今年はいよいよAI真っ盛りな一年になりそうです。

AlphaGoクローンの移植をきっかけに、せっかく、(学習じゃなくて)推論方面で色々ノウハウを持てたので、応用できたらと推論系調査備忘録。

プラットフォーム iOS/iPadOS macOS Android Windows
フラグシッププロセッサ A17 Pro M3 Snapdragon 8 Gen 3/Dimensity 9300 Ryzen 7040U/Core Ultra
上記プロセッサNPU性能 35TOPS(実力は8.75FLOPS) 18TOPS(実力は9FLOPS) 45TOPS/33TOPS 10TOPS/11TOPS
今年発売予定プロセッサ A18 M4? Snapdragon X Elite
ランタイム Core ML Core ML TensorFlow Lite DirectML
フォーマット mlmodel/mlpackage mlmodel/mlpackage tflite ONNX
変換対象 TensorFlow/PyTorch TensorFlow/PyTorch TensorFlow TensorFlow/PyTorch/...

以下、根拠のない印象。


ランタイムを使った開発の情報はあまりなく、相変わらずまだ開発者が少ない印象。

Core MLは現在、生成AI関連のウェイトをNeural Engine用にコンパイルする時間が非常に長く、推論の速さを活かすのが難しい。
AI Readyを謳おうと思うとCore MLの大改造が必要で、WWDCに向けて大忙しのはず。
メモリも今のiPhone/iPadで標準的な6〜8GBはちょっと苦しい。
今年のモデルから16GBが標準になるんじゃないか。
今のところ、Core MLやMPSGraphよりMetal生書きのサードパーティの生成AI実装のほうが優秀。

TensorFlow Lite、盛り上がっているのか不明。Pixel 8 ProやGalaxy S24のAI機能も果たしてTensorFlow Liteの上に作られているのかどうか。

DirectMLもこれから?
NPUを使おうと思うとこれを介する必要があるが、普通の開発者はNPU使わず、GPUでPyTorchおよびその関連ランタイムを使いそう。
PCメーカがAI PCをアピールするために同梱アプリを作るところから始まりそう。

SwiftのenumのrawValueは生の値じゃない

Swiftプログラミング言語が発表されて今年は10年目で、私も10年近く書いてきました。
そんな私が、「今まで何勘違いしてたの?」とショックを受けたことがこれ。

SwiftのenumのrawValueは生の値じゃない

まあ、StringをrawValueにできる時点でそういう気はしていたのですが…

enum SomeEnum: Int {
    case aCase
}

enum AnotherEnum: Int {
    case aCase
    case bCase
}

print(MemoryLayout<SomeEnum>.size, MemoryLayout<AnotherEnum>.size, MemoryLayout<Int>.size)

上記コードをPlaygroundsで走らせると、出力は、

0 1 8

となります。

caseが1つしかない時にはメモリを消費せず、caseが2つの時は1バイトです。Intのサイズ8と一致しません。

みなさん知ってましたか?


ちなみに

print(MemoryLayout<AnotherEnum?>.size)

の出力は

1

です。つまりOptionalにしても(多分、caseの数が128以下なら)メモリフットプリントは増えません。いいですね。

Optionalは9バイトになります。
Intに8バイトも普通要らないので、むしろ8バイトのOptinal_Int型が欲しいです。

MPSGraphでNeural Engineが動く!

少し前まで、MPSGraphBuilderというプロジェクトを地道に開発していました。

github.com

Core MLの.mlmodelファイルを読み込んで、MPSGraphを構築するというものです。
対応する層が限定的なものですが、おかげで、囲碁AIのLeela ZeroやKataGoのニューラルネットワークをMetal上で動かすことができました。

macOS Sonomaでmpsgraphtoolという、.mlmodelファイルを.mpsgraphpackageファイルに変換するツールが付属されるようになったので、MPSGraphBuilderの役目は終了しました。

この開発でドキュメントにない大きな知見を得たので、せっかくなので記録として残しておきます。

それは、Float16型のウェイトのMPSGraphを構築すると、AシリーズMシリーズではそれはApple Neural Engine上で動くということです。
MPSはMetal Performance Shadersの略で、MetalはGPUのドライバのブランド名なので当然GPU上で動くと思っていたのですが、MPSGraphのグラフコンパイラはデフォルトの最適化オプション(レベル1)では、使える時にはNeural Engineを使うコードを生成します。

すごくないですか?

Core MLモデルと違って、MPSGraphは動的に生成しやすいので、PyTorchやTensorFlowのバックエンドとしても利用されています。
つまり、PyTorch/TensorFlowで注意深くウェイトのデータ型をFloat16にすれば、少なくとも推論時にはGPUだけでなくNeural Engineも利用ということです。

この話、聞いたことがなくて もしかしたらどこかに落とし穴があってPyTorch/TensorFlowで使えていないのかもしれません。
実際、試しにPyTorchのresnet50をモデル、入力ともhalf()してもNeural Engineでは動きませんでした。
しかし、MPGGraphBuilderでMPGGraphがNeural Engineを使う条件が存在することは確認しました。

この話が広まって、Mac上でNeural Engineの活用がもっとカジュアルになれば幸いです。

SwiftUIのpopover

SwiftUIのpopover、なかなか手強いですね。

iPadで「できた!」と思ってもiPhoneでポップオーバーじゃなくてシートになったり。
これはiOS 16.4+ではpopoverのコンテントにpresentationCompactAdaptation(.none)修飾子をつけることで解決します。

さて、長いテキストをポップオーバーさせようとすると行数が3行程度しか表示されません。

struct ContentView: View {
    @State private var isPopoverOpen = false
    var body: some View {
        Spacer()
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                Text("""
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                        """
                )
                .fixedSize(horizontal: false, vertical: true)
                .padding()
                .presentationCompactAdaptation(.none)
                
            }
    }
}
popoverに長いテキスト

iOS16から利用可能になったLayoutプロトコルによると、SwiftUIではsizeThatFitsメソッドを通じてサイズに関して親ビューと交渉して、交渉後のサイズで子ビューをレイアウトするという仕組みらしいです。

popoverは何かの理由でこの交渉に失敗してそうです。

ではちゃんと交渉するLayoutを作りましょう。

struct PopoverContainer: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 1 else {
            fatalError("You need to implement your layout!") // 用途がレイアウトではなく交渉なので子ビューは1つに限定しています
        }
        var p = proposal
        // 提案がnil(自由にしてよい)の場合もスクリーンサイズの制約があるのでそれを採用する。
        p.width = p.width ?? UIScreen.main.bounds.width
        p.height = p.height ?? UIScreen.main.bounds.height
        return subviews[0].sizeThatFits(p) // negotiates possible size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        // 何もしないと子ビューをoriginに単に置く動作のようです
    }
}

このコンテナでpopoverのコンテントをラップします。

struct ContentView: View {
    @State private var isPopoverOpen = false
    var body: some View {
        Spacer()
        Text("Hello, World!")
            .popover(isPresented: $isPopoverOpen) {
                PopoverContainer {
                    Text("""
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                            """
                    )
                    .fixedSize(horizontal: false, vertical: true)
                    .padding()
                }
                .presentationCompactAdaptation(.none)
            }
    }
}
第1版

うーん、微妙にはみ出ました。
どうやら、スクリーンサイズからの希望では、popover側のパディング/マージンが足らず最終交渉で値切られているようです。
交渉時にpopoverに必要そうな余裕を残してみましょう。
(popover側にちゃんと最初から交渉しろよと言いたいですが…)

struct PopoverContainer: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let popoverPadding: CGFloat = 40 // これがpopover側にあげる余裕
        guard subviews.count == 1 else {
            fatalError("You need to implement your layout!")
        }
        print(#function, proposal)
        var p = proposal
        p.width = p.width ?? (UIScreen.main.bounds.width - popoverPadding)
        p.height = p.height ?? (UIScreen.main.bounds.height - popoverPadding)
        print(p, subviews[0].sizeThatFits(p))
        return subviews[0].sizeThatFits(p) // negotiates possible size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        print(#function, proposal)
        // entrusts default
    }
}

結果。

第2版

popoverPaddingがハードコードなのは気に入りませんが、とりあえずうまく行きました。

Enjoy programming!

SwiftUIの書き方

久しぶりの記事です。

ついに?SwiftUIでUIを書き始めて思うことがあったので書きます。

SwiftUIで意味のあるアプリを書こうとすると、@Stateや@Binding、@StateObject/@ObservedObject, @ObservableObject,@EnvironmentObjectと色々なプロパティラッパが出てきます。

皆さんこのラッパのネーミングに釣られて、UIのちょっとしたフラグはストラクトでStateに、ビューで表示したいデータはクラスで書き始めませんでしたか?
私はそうしました。Swiftはストラクト/プロトコル指向言語なのになんで?と思いながら。

どっぷり使うことになるだろうフレームワークを「なんで?」を抱えながらコーディングすると筋の悪いことをたくさんし始めて碌なことになりません。

で、私なりに考え直しました。
以下がそのメモです。

  1. モデルはロジック設計に任せる。SwiftUIの方言(プロパティラッパ)を絶対入れてはいけない。
  2. ビューに表示したいデータはビューモデルを別途作成する。この時必要にならない限りクラスではなくストラクトで作成する。
  3. モデルが更新されるとビューモデルが更新されるようにコーディングする。この実装には様々な方法がある。
  4. (ビューモデルにビューが1つなら)ビューには@State(ビューモデルがクラスの場合@StateObject)でビューモデルを持たせる。ビューのinitにモデルを渡してビューモデルを初期化するようにする。この時モデルをビューのプロパティにしないようにする。モデルとのやり取りの責務はビューモデルが担当する。
  5. 子ビューのユーザーイベントでモデルを変更したい時、子ビューに@Bindingで何かを渡さない。代わりにビューモデルにモデルを変更するロジックを書いて、そのクロージャをイベントアクションとして子ビューに渡す。

これでシンプルなアプリの場合には、クラスを使わずにビューモデル、ビューを記述できるはず。
なので@Binding, @StateObject/@ObservedObject, @ObservableObjectの出番はなくなります。

ただ、ビューの階層構造が深いとイベントアクションの子ビュー孫ビューひ孫ビュー...バケツリレーが面倒になるので、@EnvironmentObjectを使いたくなるケースが出てくるかもしれません。
でもできるだけ避けるほうがよいと思っています。@EnvironmentObjectを使うとその時点でビューが再利用可能でなくなりますから。
(イベントアクションについてはThe Composable Architectureにインスパイヤされました。勉強してないので、この記事が書くまでもなくTCAそのものかもしれないし、この記事が全く別のパチモンかもしれません)

もう1つ、実は2の「必要にならない限り」という条件が意外と簡単に破れるケースがあります。
それはモデルがエスケーピングクロージャーを使っている場合です。
ストラクトは、実行時に存在が保証できないのでエスケーピングクロージャーに参照型(inout)で渡すことができません。
つまり、モデルが非同期に更新された時、それをハンドラ(エスケーピングクロージャー)でストラクトのビューモデルに反映させることはできません。
この場合、ビューモデルをObservableObjectとしてクラスで設計する必要があります。

今回の主旨は、「プロパティラッパの名前に釣られて安易にクラスに流れるな」「クラスを使わずストラクトでもSwiftUIは結構書ける」でした。

Maxima日本語マニュアルのソース(texi)を公開しました

どうやらMaxima 5.42.2で力尽きたようなので😅、texiファイルを公開しました。

github.com

奇特な方がおられましたら、forkして引き続き更新いただければ幸いです。

Android版Firefoxでリモートデバッグ

AndroidFirefoxのリモートデバッグをしようとしたら苦労したので備忘録です。

ポイント

  • AndroidFIrefoxは2020年7月16日現在バージョン68である。
  • PC版Firefoxは2020年7月16日現在バージョン78である。
  • バージョンの違いが10あるためこの組み合わせでリモートデバッグはできない。
  • PC版FirefoxのExtended Support Release(ESR)に68ベースのものがあるのでそれを使うことでリモートデバッグが可能になる。

手順

重要 PC版Firefox ESRのabout:configにて`devtools.aboutdebugging.new-enabled`を`true`にセットする。
あとはドキュメントに従う

developer.mozilla.org

2020年7月16日現在、日本語のドキュメントはバージョン36以前のものなので読まないこと。