Hatena::Grouprekken

murawaki の雑記

2011-01-22

Rendering Traditional Mongolian with Pango

Linux (UNIX) 上で縦書きモンゴル文字を表示する方法。現実逃避の成果 (?)。font の仕組みも何もかもわかっていないので、いろいろ調べながら。

Linux 上でテキストの rendering を行っているのは pango。現行版はモンゴル文字をサポートしていない。パッチがあったので試してみた。フォントとしては Windows 7 についてくる Mongolian Baiti を使う。よくわかんないけど、ライセンス違反でないことを願う。

2011/01/23 追記: 実はサポート済みとのこと。バグ報告が長らく止まっていると思ったら、大改造が進行中らしい。shaper は harfbuzz 側でサポートされているとのこと。確かに harfbuzz の hb-ot-shape-complex-arabic.cc に記述されていた。2010-12-21 の commit。アラビア文字などと一緒に処理されている。U+180E MONGOLIAN VOWEL SEPARATOR は相変わらずスペースとして表示されないらしい。そのうち harfbuzz を試してみるかもしれない。なにしろ基盤層をいじるので、環境構築が大変そう。

見つかったパッチは 3種類。github にあがっているのと、pango のバグ報告に添付されているのが2種類。ソースのコメントによると、作者は全部 Erdene-Ochir Tuguldur という人。github のが最新版のようだが、とりあえずバグ報告の2番目のパッチを使う。

基本的な手順はブログ記事にある通り。Ubuntu 上で行うなら。package 化されたの pango1.0 の source を取ってきて、patch をあてて debuild。必要なツールは apt-get install しろと上記の記事には書いてある。他にも gtk-doc-tools を install しないと configure.in からの生成に失敗する。aclocal, autoconf, automake -a が必要っぽい気がする。エラーが出て試行錯誤したので本当のところは分からなくなってしまった。

rendering のテストには Mongolian Unicode Test Page が使える。だいたいうまく表示される。大きな問題は二つ。制御文字の MVS (Mongolian Vowel Separator) がそのまま表示される。一か所だけ FVS1 (これも制御文字) が表示される。これらは後でもう一度取り上げる。

パッチの中身。モンゴル文字処理用に mongolian という module を追加している。モンゴル文字は、孤立形、語頭形、語中形、語末形を持つが、これを処理しているだけ。これらは OpenType font の Glyph Substitution の feature で制御される。対応する feature は isol, init, medi, fina。そのいずれかをテキスト中の各文字に付与。

実際には、文脈に応じてもっと多くの異形態がある。孤立形、語頭形、語中形、語末形の4種類ではすまない。ligature もある。どうしてこんな単純な処理だけでうまくいくのか。実は glyph 選択規則はほとんど font 側に書いてあった。モンゴル文字用に特別な処理を書かなくても、pango の OpenType 一般のルーチンで処理できている。

これを確認するために寄り道。fontforge という package を取ってくる。fontforge は font editor。編集する用がなくても、browser として使える。fontforge に monbaiti.ttf を与えて、Mongolian Baiti を眺める。

適当にモンゴル文字を選んで context menu から Glyph Info を選ぶ。Substitutions を見る。LETTER A の場合、Subtable の一つとして 'init' Initial Forms in Mongolian lookup 9 subtable がある。これに対応する Replacement Glyph Name が uni1820.init。語頭形なら語頭形の glyph を使うということ。isol, fina, medi, init は通常の gsub feature。OpenType の Single Substitution というやつ。substitution の選択は mongolian module がやっている。

LETTER A の Subtable には Single Substitution lookup 94 subtable というものもある。対応する Replacement Glyph Info は uni1820.mvsvar1。どうやって使うのか。各 code point から Glyph Info を見ていてもわからない。menu の Fonto Info... から Lookups に移って GSUB の一覧を見るとわかる。

Substitution には Ligature Substitution というものもある。FVS を使った強制的な異形態表示は ligature で処理されていた。Standardized Variants にある規則はフォントに書いてある。ᠨᠠ᠋ᠨ (n-a-FVS1-n) とかやると、LETTER A が uni1820.medivar1 で表示される。Glyph Substitution Table に書いてある通り。

Chaining Substitution はもっと複雑なパターンマッチング。解読するのが大変。

ちゃんと動作確認するために、Unicode 5.2 の 13.2 Mongolian に書いてある例を片っ端から試してみる。

U+200D ZERO WIDTH JOINER は正しく処理されない。全部 isolate。

  • isolate
  • ᠠ‍ initial
  • ‍ᠠ final
  • ‍ᠠ‍ medial

Uniscribe でもそれぞれ isolate, initial, isolate, initial が表示された。手前の ZWJ が処理されてない。

FVS はうまくいく。

  • ᠭᠠᠯ (gal with dots)
  • ᠭ᠋ᠠᠯ (gal without dots)。

母音調和の処理もうまくいく。

  • ᠵᠠᠷᠯᠢᠭ jarlig (masculine g)
  • ᠴᠢᠷᠢᠭ chirig (feminine g)

最後の g の shape を区別するには、上の例では母音 a の出現を、下の例では i 以外の母音が出現しないことを認識する必要がある。かなり長距離の依存。g には Chaining Substitution の rule がいっぱい書いてある。そのどれかが対応しているのだろう。

MVS。

  • ᠬᠠᠨᠠ xana
  • ᠬᠠᠨ᠎ᠠ xan-a

n の shape は正しい。Chaining Substitution が効いている。MVS をそのまま表示するのがまずい。

再び Mongolian Unicode Test Page の表示について。

  • ᠲᠥᠷᠥᠯ töröl。最初の ö は牙 (silbi) 付きで二番目は牙なし。ü/ö のデフォルトの medi は牙なし。どの規則で牙ありにしてるのかと思ったら、contextual 50。initial の直後の ü/ö だけヒゲを生やす。この規則はモンゴル語の語頭に重子音が来ないことに頼っている。外来語で CC + ü という構造を探してみたところ、見つかったのは ᠺᠯᠦᠪ (klüb) だけ。案の定牙なしで表示される。ᠺᠯᠦ᠋ᠪ と FVS1 を挿入したらいけた。
  • ᠪᠤᠢ ᠵ᠎ᠠ bui-ǰ-a。元のページでは j の shape がおかしいが、コピペすると正しく表示されるようになった。謎。
  • ᠰᠣᠨᠣᠰᠲᠠᠭᠰᠠᠨ᠋᠎ᠠ sonostaɣsan-a。これはよくわからない。final の n に FVS1 を加える規則は定義されていない。おかしいのはテキストの方。rendering engine は単に FVS1 を無視すればよい。
  • ᠰᠠᠢᠨ sain。以前も取り上げた語。牙が2本欲しいが1本しか表示されない。Mongolian Baiti の仕様通りの振る舞い。

まとめると、意外とうまくいっている。問題は2点。U+200D ZERO WIDTH JOINER を処理していないことと制御文字をそのまま表示していること。複雑な規則は font に書いてある。text rendering 側でやるべきことは多くない。font を作るのは本当に大変そう。一応は表音文字なのに。

そういえば、禁則処理とかはどうなっているのだろうか。

2012年10月9日追記: ブログ記事によると、Linux の Firefox がようやくモンゴル文字サポートの入った Harfbuzz を使うようになったとのこと。ひとまずめでたい。まあ一番の問題はフォントなわけだが。

2010-10-22

Traditional Mongolian support in Windows 7

縦書きモンゴル文字の入力について少し調べてみた。完全な現実逃避

歴史を遡ると、Unicodeモンゴル文字が追加されたのが version 3.0。なんと1999年。しかし長らく誰もサポートせずに放置されてきた。理由の一つは実装が難しいこと。縦書きで左から右に改行 (日本語と反対)。アラビア文字のように文脈によって字形が変化する。もう一つは Unicode の仕様がひどいこと。文字の割り当てがやっつけ仕事っぽい。おまけに仕様が不明。入力したいモンゴル語の単語があったとき、どのコードポイントの文字の連続で表現するのが正しいのかよくわからない。

Windows Vistaモンゴル文字をサポートしたという話は聞いていたが、以前 Firefox で試しに表示すると全然駄目だった。(今表示したらうまくいった。XP と勘違いしたのかも。) 何が駄目かというと字形の選択。同じコードポイントでも前後の文字に応じてグリフを変えないといけないのに、それが出来ていない。誰が犯人かは特定していない。Windows 版の Firefox は内部で Uniscribe を呼び出してレンダリングしている。こいつが悪いのか、あるいはフォントに文脈依存のグリフが登録されていないのか。

最近 Windows 7 で試してみた。どうやらちゃんと表示される様子。おまけに入力メソッドが標準で入っている。登録方法は以下の通り。コントロールパネル -> 地域と言語 -> キーボードと言語 とたどる。「キーボードの変更」ボタンから「モンゴル語 (伝統的なモンゴル文字中国)」をリストに追加。

キーの配置は「キーボード レイアウトプレビュー」で一応表示される。しかし問題が多い。そもそも文字が小さすぎて判別できない。表示されているのは通常のキーだけで、Shift キーを伴なう場合がわからない。特殊な制御文字 MVS, FVS1, FVS2, FVS3 が表示されない。*1そもそもモンゴル文字が曖昧なので、グリフを見せられても対応するコードポイントの判別ができないものがある。ドキュメントを探してみたが見当たらない。そこでまずキー配置について気付いたことを挙げる。

  • 全体としてラテン文字の転写に近い。例えば m のキーで (m と転写される) が入力される。キリル文字キーボード準拠の配置ではない。
  • まず気になるのが四つの円唇母音。w が o。v が u。u が ü。o が ö に対応。これはわかりにくい。
  • W が v。E が外来語用の ê。Unicode は両者に別のコードポイントを割り当てる。一般的には v を母音表記に転用したという解釈をとると思う。でも確かに語頭の字形は違う。語頭で ê は冠 (titim) をとるけど、v はとらない。
  • q が č。c が ts。
  • N が ng。g が ɣ/g。h が q/k。
  • H が h。k が g̣。K が ḳ。
  • L が lh。R が ř。x が š。z が ž。
  • - が NNBS (Narrow No-break Space)。接尾辞分離用。
  • 日本語キーボードで、MVS が =、FVS1 が :、FVS2 が *、FVS3 が半角/全角。

これで解決したかというとそうではない。例えば、キリル文字сайн と綴る単語をどう表現したらいいのかわからない。この単語のモンゴル文字綴りに対する一般的な転写は sayin。ayi で二重母音を表す。yi と転写するけど、字形は長い牙 (silbi) が2本連続したもの。これを ai と転写する流儀もある。i は a に後続するとき長い牙2本という形を取るという解釈。つまり yi と見るのは一つの解釈に過ぎない。モンゴル文字の表記は同じでも、sayin と解釈する人もいれば sain と写す人もいる。では Unicode はどういう立場なのかというとはっきりしない。Unicode 的には文字と言語は別なのだろうが、モンゴル文字のように両者の結びつきが強いと、そうも言ってられない。

実際に入力してレンダリングを確かめてみる。ᠰᠠᠢᠨ (s a i n) とすると、silbi が1本。ᠰᠠᠶᠢᠨ (s a y i n) とすると、silbi が2本。表示としては後者が正しい。しかし Google で検索すると 77 対 8 で前者の方が多い。前者の検索結果の中に正しく表示されているものがあった。どうしているのかと思ったら、ᠰᠠᠢ᠌ᠨ (s a i FVS2 n) と FVS2 を使っていた。どうやら Google は制御文字を飛ばしてインデキシングしているようだ。しかし、こんな規則的な綴りに対して本当に Free Variation Selector が必要なのだろうか。ちなみに ᠰᠠᠢᠢᠨ (s a i i n) とすると 85 件ヒット。これが一番多い。

別の例。キリル文字Ордос と綴る地名。この地名のモンゴル文字綴りの転写には流儀が二つある。ordus と ordos。そもそもモンゴル文字の o と u が同一字形。というよりも、一つの文字を文脈によって o か u と解釈しているというのが実態。Unicode は両者に別のコードポイントを割り当てている。表示が同じでも、実際の発音に応じて使い分けろということ。そこで問題となるのが第二音節以降の o と u の区別。第一音節では o と u は区別されるが、第二音節以降の短母音は弱化して区別がなくなる。ではどうするかというと流儀が二つあって、一つは機械的に u で転写するというもの。もうひとつは条件によっては o と写すもの。

やはり実際に入力してみる。前者が ᠣᠷᠳᠤᠰ (o r d u s)。後者が ᠣᠷᠳᠣᠰ (o r d o s)。レンダリングはどちらも問題ない。Google 検索では後者が圧倒的に多いが、Wikipedia で使われていて拡散しているというバイアスがかかっているから判断を保留。

昔のモンゴル文字の character set は glyph set であった。印刷する分には問題ないが、せっかく電子化しているのに sort もできず不便ということで、符号とグリフを分離したのが Unicode の Mongolian。それなのに、モンゴル文字表記に対する解釈が一定せず、てんでバラバラな符号化が行われている。これでは意味がない。

2011年1月14日 追記: このブログによると、Linux の pango でも、パッチをあてれば一応表示できるらしい。Windows の Baiti フォントを使うとのことだけど、ライセンスは大丈夫なのだろうか。github にあるソースを見たところ、語頭、語中、語末の識別しかやってない。明らかに不完全。スクリーンショットを見ると、MVS や FVS に対応していないのは明らか。でも ligature は表示できているように見える。なぜ? フォントのことが分かってなさすぎる。

2011年1月22日 追記: pango の話は記事にした。

*1:Shift と MVS に関しては、ネット上で拾った画像が不完全だが参考になる。

2010-06-10

gxp for NLPers

gxp は並列処理用のツール。これを使って NLP で並列処理を行ってきた。その話をまとめてみるテスト

gxp については、既にNLP2010 のチュートリアルで開発者自身による解説があった。ただ、NLP の人間との間に問題意識にずれを感じた。これについては簡単な報告が既にある。

gxp を使い始めた理由。研究室が既に使っていたから。今でも使っている理由。それなりに使えているから。いろいろ泥臭いことをやりつつ。

議論には比較対象があるとやりやすい。今なら MapReduceHadoop。しかし Hadoop を使ったことはない。象本は積ん読状態。Google の元論文を研究室で紹介したことはあるが、もう3年も前。聞きかじった情報だけで比較してみる。

gxp がやること

処理を行うための計算機が複数あるとき、gxp はまずそれらを制御下に置く。具体的には、各計算機にデーモンを走らせる。デーモン同士はネットワークでつながっている。全体で木を成す。

利用者は、制御下に入った計算機に対して、gxp を介して手元の端末から命令 (シェルコマンド) を出す。命令はデーモン間でリレーされる。デーモンは自分が実行すべき命令を実行する。命令の種類はいろいろ。一斉に同じ命令を実行したり、タスクの一覧を与えて、それらをスケジューラが適当にばらまいたり。

gxp の利点

ほとんどの環境で動く。ヘテロな環境でも動く。しかも簡単にインストールできる。

ほとんどの環境で動く。gxpPython で書かれている。処理に使いたい各計算機で Python が動けばよい。UNIX を想定してコーディングされているけど、Mac でも動かせそう。多分。

簡単にインストールできる。手元の計算機ひとつだけに gxpインストールすればよい。gxp は他の計算機を獲得する (制御下に置く) 際に自身をコピーする。だからあらかじめ各計算機に gxp をばらまく必要がない。そこに計算機があって、ネットワーク越しにアクセスできるなら、もう gxp が使える。

ヘテロな環境でも動く。デーモンネットワークを作るには、ある計算機から別の計算機へアクセスできる必要がある。gxp ではこのネットワーク接続の部分が抽象化されている。普通の計算機環境なら、sshログインする。他の接続方法も実装されている。

大型計算機にも対応している。大型計算機では、通常バッチキューイングシステムを使わないと処理を実行できない。どうやっているかというと、gxp が普通にキューにジョブを登録する。普通は本当に計算したい処理をキューに登録するところだが、gxp が登録するジョブは、デーモンを立ち上げるためのスクリプト。しばらく待って大型計算機のスケジューラによってジョブが適当な計算機に割り当てられる。すると、デーモンが立ち上がって親計算機と接続する。つまり、単にデーモンが待機しているだけで、本当に実行したいタスクはまだ走っていない状態になる。大型計算機から見るとジョブが走っているように見える。

複数の接続方法を組み合わせることもできる。自分のクラスタでは ssh で接続して、同時にバッチキューイングシステムを通して大型計算機を使うこともできる。ただし、計算ができるというだけで、効率よく計算できるかはまた別の話。

この利点を生かせる条件。自分で Hadoop のようなミドルウェアを好きにインストールできない環境。あるいはそうした計算機も使いたい場合。または、そうした準備がめんどくさいので手っ取り早く始めたい場合。最近は自前でクラスタを持っている研究室が少なくないので、このありがたみは薄れているかもしれない。

なお、ほとんどの環境で動くといったものの、Pythonライブラリバージョンアップで頻繁に変更される影響で、warning が出たり、コードの手直しが必要なことが時々ある。

ep

2012年5月1日追記: 元々開発者推奨ではなかった ep ですが、最新版では使えなくなっています。古いバージョンを取ってくると使えます。2010年頃。

ep の代替機能は js が提供しています。

gxpc js -a work_file=tasks

と実行します。tasks は、実行したいコマンドリストを書いたファイルです。ただしフォーマットが変わっています。ep 版では「ID command」とコマンドの前に ID を書きますが、js 版ではコマンドだけです。つまり (人手で付与した) ID によるタスクの管理が廃止されています。そのため、途中までしか終わっていないタスクを再開するのが面倒になりました。ep 版では実行結果のステータスが status ファイルに吐かれており、ep が起動時にこれを参照するので、実行済みのタスクは再実行されませんでした。js ではそうした機能がないので、tasks ファイルから実行済みのタスクを取り除く必要があります。このように、不便になったので、最近はもっぱら gxp make を使っています。

gxp を使うと、もともと並列処理用に記述していないプログラムを並列処理に転用しやすい。対する MapReduceプログラミングパラダイム。基本的には mapper と reducer を新たに書くことになるはず。シェルコマンドも実行できるらしいけど。

NLP でよくあるのは、データの加工。データは分割できて、その断片間には依存関係がないので簡単に並列化できるという設定。依存関係が複雑な場合は MPI でも使ってください。

まず小さなデータで試して成功したら、次は巨大なデータで試すという流れ。

具体例。テキスト中の単語を数えてハッシュに保存するプログラムを考える。

count_word --input input.txt --output output.db

これを並列化する。データが 00000 から 09999 まで1万個あるとする。最終的に欲しいのは、これら全体から得られる頻度データベース

まず、count_word を1万回実行する。パラメータだけを変えて。1万個のタスクは互いに独立なので並列に実行できる。これを実現する gxp の機能が epep は embarrassingly parallel から来てるらしい。

タスク一覧を書いてスケジューラに渡すと、スケジューラが空いている計算機に適当にタスクを割り振る。タスクの一覧は以下のように記述。

00000 count_word --input 00000.txt --output 00000.db
00001 count_word --input 00001.txt --output 00001.db
....
09999 count_word --input 09999.txt --output 09999.db

1行1タスク。行の最初はタスクのID。既存のプログラムが使い回せている。

次に、得られたデータベースをマージする。ここでデータベースをマージするプログラムを新たに書かなければならない。仕方がないと割り切る。

merge_db --start 00000 --end 09999 --output all.db

1万個を順番にマージしていては遅い場合がある。この場合、例えば、2段階のマージを行う。1段階目のマージは、ep で並列実行。以下のようにタスク一覧を記述。

00 merge_db --start 00000 --end 00999 --output 00.db
01 merge_db --start 01000 --end 01999 --output 01.db
....
09 merge_db --start 09000 --end 09999 --output 09.db

次に2段階目のマージを行い最終結果を得る。

merge_db --start 00 --end 09 --output all.db

改めて MapReduce との比較。count_word は map を行い、ローカルに reduce を行っている。処理の独立性が高いので map を書くのは簡単。面倒なのは map された結果を shuffle して、reduce する部分。shuffle を利用者から隠蔽したのは MapReduce の良いところ。gxpep は、この部分の面倒を見てくれない。仕方がないので自分で泥臭い処理を書くことになる。今回は最終出力を一つのデータベースとしたが、例えば、これが巨大すぎるので分割して持たなければならないとなると、一気に処理が面倒になる。

ep はそこそこスケールする。10,000個のタスクを500並列ぐらいで実行するなら問題なく動く。IO の問題を別にすれば (後述)。スケジューラがボトルネックになるとすれば、1秒で終わるタスクが山程あるばあい。数分、あるいは数時間かかるタスクであれば問題ない。

gxp make

gxp で並列処理をする場合、昔は ep しかなかったが、最近は gxp make というものが開発されている。現在の開発者のおすすめは gxp make。epチュートリアルにも載っていない。しかし、このあたり、並列分散処理の研究者自然言語処理研究者が同床異夢な部分ではないかと個人的に思っている。

gxp make とは何か。make を透過的に並列化してくれる。普段 make コマンドをたたいて実行するところを gxpc make -j とするだけで、途中の処理を各計算機に割り振って並列に処理してくれる。

make を使う利点。複雑な処理の依存関係を記述できる。入力を処理して、その出力をまた別の処理にかけて、その結果が分岐して、処理Aと処理Bに別々にかけるといった感じ。こうした依存関係を Makefile に記述しておけば、gxp make が自動的に最後まで処理してくれる。

しかし、この機能は、並列分散処理の人が思っているほど NLPer にとって重要ではないのではないか。多分、並列分散処理の人は、NLP を含む「アプリケーション」の人から完成された処理内容をもらって、それをどう効率よく実行するか研究しているのだと思う。一方、「アプリケーション」の人は、処理内容を作ること自体が研究。研究段階では普通は workflow は完成していない。各段階で出力を人手で確認しては、プログラムを修正するといったことを繰り返す。つまり workflow を一気に処理する必要がない。逆に、一気に flow を処理できる段階になったら研究としては終わっている。この完成段階で並列計算に要求があるとすれば効率。しかし make の利点は簡単に記述できることであって、効率面では期待できない。

gxp make の問題。一つはファイルシステムの問題。これは後述。もう一つはスケールしないこと。少なくとも昔聞いた話では、タスクごとに手元の計算機に対応するプロセスが作られるから、そこがボトルネックになる。10,000個のタスクはとても捌けないということだった。

私は大規模処理には相変わらず ep を使っている。理由はいろいろ。一つは、タスクの数が固定だから。1億ページのコーパスがあって、これを1万ページのブロック1万個に分割して管理している。このコーパスから様々なデータを抽出している。1万個のタスクを処理する必要があるとわかっているのだから、タスクファイルを記述するのが自然。

gxp make は小規模な処理に使っている。並列数10ぐらい。とても大規模とは言えない。gxp make が便利なのは、タスクの数が不定の場合。上のコーパスの例ではタスクの数が10,000と決まっていた。そうではない場合。make なのでファイルの suffix を元にデータを管理する。ある特定の suffix のファイルを追加したら、その差分だけ再実行してほしいといった場合。make が自然に処理してくれる。

別の use case。評価実験の実行に使っている。何のことはない普通の Makefile。workflow は以下の通り。

  1. データを訓練データをテストデータに分割。
  2. 訓練データから学習してモデルファイルを出力。これを複数のアルゴリズムの学習器について実行。
  3. 各モデルファイルをテストデータにかけて結果をファイルに出力。

ここで、例えば、分類器として Passive-Aggressive を使っていたが、新たに Confidence Weighted を実装したとする。前者を suffix .pa、後者を .cw で管理するとする。Makefile に .pa と同様の依存関係を .cw について追加して make すると、Confidence Weighted の部分だけが実行される。これぐらいの規模だと、逐次実行しても問題ないが、締め切り前で急いでいるときに並列に実行したりする。

2012年5月1日追記: gxp make については、構造化パーセプトロンの並列学習の記事でも触れた。

2012年5月1日追記: make の欠点はデバッグが難しいこと。期待通りに動かなかったときに、何が原因か解明するのに時間がかかってしかたがない。良いデバッグ方法を知っていたら教えてほしい。

データ管理の問題

NLP における並列処理はデータの管理につきる。計算は単純に並列化すればいい。ボトルネックとなるのは IO。

例えば NFS で共有されているディスクに一斉にアクセスすると、数十並列で破綻する。ではどうしようかということになるが、gxp は基本的には IO の面倒は見てくれない。gxp make がスケールしないもう一つの理由は、計算機間で NFS でファイルが共有されている状態を想定しているから。とりあえず、ここから先は ep のみを考える。

問題の切り分け。タスクによって、入力が大きく、出力が小さい場合と、入力も出力も巨大な場合がある。例えば、単語のカウントだと、出力は入力に比べて圧倒的に小さい。テキストを構文解析する場合、出力は膨れ上がる。

出力が小さければ、一カ所に吐いても問題ないことが多い。もっといえば、スループットが許容範囲内かが問題。どう実行するかを決めるのは計算機のご機嫌。あと、ちびちびデータを転送していたら効率が悪そうなので、各計算機でローカルに出力をキャッシュしておいて、処理の最後に一気に出力する。ちゃんと速度を比較したことはないけど。

出力は共有ディスクに吐くとして、入力はどうするか。いろいろ試行錯誤した。初期の方法。計算機のローカルディスクの同じパスに同じファイルをばらまく。例えば、各ファイルの複製を3個作るとする。タスク記述に制限を加えて、複製を持つ計算機だけが該当タスクを実行するようにする。

00000 :on (hostA00|hostB01|hostC02); count_word --input /local/00000.txt --output /shared/00000.db
00001 :on (hostA01|hostB02|hostC03); count_word --input /local/00001.txt --output /shared/00001.db
....
09999 :on (hostA09|hostB03|hostC04); count_word --input /local/09999.txt --output /shared/09999.db

on は正規表現にマッチするホストだけで実行できることを記述する。00000.txt は hostA00 と hostB01 と hostC02 のローカルディスクに置いてある。

この方法は欠点が多い。まず、データを自分でばらまいて、どこに何があるのか把握しておくのは面倒。時間もかかる。同じデータを何度も使いまわすのでなければやってられない。次に、一部の計算機がダウンすると実行バランスが崩れる。それに、処理の最終局面で効率が悪い。処理できるデータがなくて遊んでいる計算機が増えてくる。

最後の問題へのやっつけ対策。sshfs を利用。入力は相変わらず各計算機にばらまいてあるが、スケジューリングで実行ホストを制限しない。タスクを割り当てられた計算機がデータを持っていればそれでよし。なければ持っている他の計算機に sshfs でアクセス。当然ローカルディスクよりも遥かに遅い。しかし、タスク一覧の最後の方を sshfs 方式にしておくと、最終局面で遊んでいる計算機が減る。しかし面倒。それに、大型計算機センターだと、計算ノードから外側にアクセスできないことが多くて、仮に FUSE をサポートしていても sshfs が使えないはず。

最近の対処法。gfarm を利用。gfarm分散ファイルシステム。複数の計算機のディスクを仮想的に一つのファイルシステムとして見せる。GoogleFS と違って POSIXAPI を一通り実装している。普通に mount できる。

gfarm を使うと、計算機のローカルディスクにファイルをばらまくという処理をこのミドルウェアに任せられる。もっとも、gfarm は自動で複製を作ってくれない。2012年5月1日追記: 新しめの gfarm ではマウント時に gfarm2fs -o ncopy=3 /mount/point と指定すると、ファイル作成時に自動的に複製される。複製を作るコマンドは gfrep。これを自分でたたく。計算機が死にまくると、アクセスできる複製数が0になることがある。そうならないように定期的にチェックして、やばかったら複製を増やす必要がある。

gfarm によって入力データが計算機間で適当にばらまかれたとする。epgfarm 上に一斉にアクセスしても、実際のデータへのアクセスはばらける。入力だけでなく、大きめの出力も gfarm 上に吐けばいい。吐いたら gfrep で複製。

しかし、実際にやってみると ep 起動直後に実行されるタスクでファイルの open か read に失敗する例がちらほら。どこかで TCP の接続の上限とかに引っかかっているような気がする。要検証。

やったことはないが、gfarm を使えば、gxp make もスケールするのではないか。上述のプロセス数の上限までは。

問題は効率。ep のスケジューラが gfarm と連動していない。効率を上げるには、タスクを実行する計算機がデータを持つ計算機と同じか、ネットワーク的に近いべき。現状は、そんなことを考慮せず、適当に実行する計算機を決めている。

解決策。資源の近さを考慮したスケジューラを書けばよい。ファイルの実体の所在は gfwhere コマンドでわかる。それを使えばよい。しかし、実装はそんなに簡単ではない。そもそもここまでくると私の仕事ではない。Hadoop ならこの部分を自動でやってくれるはず。どれぐらいうまくやってくれるのか知らないが。

2010-05-12

cron on cygwin

cygwin で cron を動かそうとしてはまったのでメモ。

はまった点は三つ。複合的なので症状と原因の整理はできていない。

  1. cron が起動時にエラーを吐く。
  2. crontab がすぐに反映されない。
  3. 通常と環境が違う。ssh-agent を反映させるのが面倒。

サービスの登録には普通は cygrunsrv を使う。sshd はこの方法でうまく行った。ただし、Windows 7 では「管理者として実行」する必要がある。

以下のコマンドの場合、エラーメッセージが返ってくる。

cygrunsrv --install cron --path /usr/sbin/cron --args -D
cygrunsrv --start cron

いろいろ調べたところから推測すると、利用者 SYSTEM として実行しようとすると不都合な環境であるらしい。

解決策。/usr/bin/cron_diagnose.sh という便利なスクリプトを実行。run as yourself という質問に対して yes と答える。これで cron が私の権限で実行されるようになった。ここでも「管理者として実行」が必要。

二つ目の問題は、Wikipedia に「よくあるミス」として載っていた。デバッグのために cron の実行時刻として 40 秒先とかを指定して crontab で登録すると、すぐには実行されない。実行時刻を少し先にすると解決。

最後の問題。cron の実行は環境設定が異なる。そのため、ssh を使う処理は、ssh-agent の chain が切れているので失敗する。keychain という utility を使えばいいらしいことを知ったが、いつものように環境変数 SSH_AUTH_SOCK を自力で設定することによって解決。

2010-04-07

Migrating from Subversion to git

二つの作業のログ。(1) Subversion から git への移行。(2) 新たな git repository 二つを merge。

やりたいこと。Subversion から git への移行。Subversion 時代は自分用のレポジトリを二つ使用。相互に関連しているので、移行を機に統合。

Subversion から git への移行には svn2git を使う。git-svn だとずっと Subversion に貼り付きそうだが、目的は Subversion とおさらばすること。誤解かもしれない。ちゃんと調べたわけではない。

svn2git を gem で取ってくる。

gem install svn2git --source http://gemcutter.org

Subversion の repository には、svn というアカウントを作って svn+ssh でアクセスしていた。二つの repository は、projectAprojectB とする。

projectA を git に移行。projectB も同様。

mkdir projectA
cd projectA
svn2git svn+ssh://svn@repo-host/projectA --rootistrunk

なぜ --rootistrunk でうまくいくのかわかっていない。ファイルは current working directory 直下に生成される。適当なディレクトリを作ってからそこで実行する。

projectAprojectB を merge。とりあえず projectA/projectB というディレクトリ構成にする。気に入らない部分は後で移動させればよい。

cd projectA-git-path
git remote add -f projectB projectB-git-path;
git merge -s ours --no-commit projectB/master;
git read-tree --prefix=projectB -u projectB/master;

そして git commit。これで projectA でも projectB の履歴を引き継げている。

projectA 側で projectB を track する必要はない。縁を切る。

git branch -rd projectB/master

.git/config に svn や projectB の痕跡が残っているが問題なかろう。git branch -a では出てこない。

git clone --bare でハブとなる repository を作る。こちらの .git/config には svn や projectB の痕跡は残っていない。

svn:ignore の情報は引き継がれない。手動で等価な .gitignore を作る。